Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp =================================================================== --- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 11555) +++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 11556) @@ -1,634 +1,644 @@ /* Copyright (C) 2012 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 "scriptinterface/ScriptInterface.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "gui/GUIManager.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/sysdep/sysdep.h" #include "maths/FixedVector3D.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetTurnManager.h" #include "ps/CLogger.h" #include "ps/CConsole.h" #include "ps/Errors.h" #include "ps/Game.h" #include "ps/World.h" #include "ps/Hotkey.h" #include "ps/Overlay.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" #include "ps/SavedGame.h" #include "ps/UserReport.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpGuiInterface.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/helpers/Selection.h" #include "js/jsapi.h" /* * This file defines a set of functions that are available to GUI scripts, to allow * interaction with the rest of the engine. * Functions are exposed to scripts within the global object 'Engine', so * scripts should call "Engine.FunctionName(...)" etc. */ extern void restart_mainloop_in_atlas(); // from main.cpp namespace { CScriptVal GetActiveGui(void* UNUSED(cbdata)) { return OBJECT_TO_JSVAL(g_GUI->GetScriptObject()); } void PushGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData) { g_GUI->PushPage(name, initData); } void SwitchGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData) { g_GUI->SwitchPage(name, initData); } void PopGuiPage(void* UNUSED(cbdata)) { g_GUI->PopPage(); } CScriptVal GuiInterfaceCall(void* cbdata, std::wstring name, CScriptVal data) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return JSVAL_VOID; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY); if (!cmpGuiInterface) return JSVAL_VOID; int player = -1; if (g_Game) player = g_Game->GetPlayerID(); CScriptValRooted arg (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), data.get())); CScriptVal ret (cmpGuiInterface->ScriptCall(player, name, arg.get())); return guiManager->GetScriptInterface().CloneValueFromOtherContext(sim->GetScriptInterface(), ret.get()); } void PostNetworkCommand(void* cbdata, CScriptVal cmd) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY); if (!cmpCommandQueue) return; jsval cmd2 = sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), cmd.get()); cmpCommandQueue->PostNetworkCommand(cmd2); } std::vector PickEntitiesAtPoint(void* UNUSED(cbdata), int x, int y) { return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false); } std::vector PickFriendlyEntitiesInRect(void* UNUSED(cbdata), int x0, int y0, int x1, int y1, int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false); } std::vector PickFriendlyEntitiesOnScreen(void* cbdata, int player) { return PickFriendlyEntitiesInRect(cbdata, 0, 0, g_xres, g_yres, player); } std::vector PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank) { return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false); } CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), 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)); } std::wstring SetCursor(void* UNUSED(cbdata), std::wstring name) { std::wstring old = g_CursorName; g_CursorName = name; return old; } int GetPlayerID(void* UNUSED(cbdata)) { if (g_Game) return g_Game->GetPlayerID(); return -1; } std::wstring GetDefaultPlayerName(void* UNUSED(cbdata)) { // TODO: this should come from a config file or something std::wstring name = sys_get_user_name(); if (name.empty()) name = L"anonymous"; return name; } void StartNetworkGame(void* UNUSED(cbdata)) { ENSURE(g_NetServer); g_NetServer->StartGame(); } void StartGame(void* cbdata, CScriptVal attribs, int playerID) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); CScriptValRooted gameAttribs (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), attribs.get())); g_Game->SetPlayerID(playerID); g_Game->StartGame(gameAttribs, ""); } CScriptVal StartSavedGame(void* cbdata, std::wstring name) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); // Load the saved game data from disk CScriptValRooted metadata; std::string savedState; Status err = SavedGames::Load(name, guiManager->GetScriptInterface(), metadata, savedState); if (err < 0) return CScriptVal(); g_Game = new CGame(); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); CScriptValRooted gameMetadata (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), metadata.get())); CScriptValRooted gameInitAttributes; sim->GetScriptInterface().GetProperty(gameMetadata.get(), "initAttributes", gameInitAttributes); int playerID; sim->GetScriptInterface().GetProperty(gameMetadata.get(), "player", playerID); // Start the game g_Game->SetPlayerID(playerID); g_Game->StartGame(gameInitAttributes, savedState); return metadata.get(); } void SaveGame(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); if (SavedGames::Save(L"quicksave", *g_Game->GetSimulation2(), guiManager, g_Game->GetPlayerID()) < 0) LOGERROR(L"Failed to save game"); } void SetNetworkGameAttributes(void* cbdata, CScriptVal attribs) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(g_NetServer); g_NetServer->UpdateGameAttributes(attribs, guiManager->GetScriptInterface()); } void StartNetworkHost(void* cbdata, std::wstring playerName) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_NetServer = new CNetServer(); if (!g_NetServer->SetupConnection()) { guiManager->GetScriptInterface().ReportError("Failed to start server"); SAFE_DELETE(g_NetServer); return; } g_Game = new CGame(); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection("127.0.0.1")) { guiManager->GetScriptInterface().ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void StartNetworkJoin(void* cbdata, std::wstring playerName, std::string serverAddress) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_Game = new CGame(); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection(serverAddress)) { guiManager->GetScriptInterface().ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void DisconnectNetworkGame(void* UNUSED(cbdata)) { // TODO: we ought to do async reliable disconnections SAFE_DELETE(g_NetServer); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } CScriptVal PollNetworkClient(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); if (!g_NetClient) return CScriptVal(); CScriptValRooted poll = g_NetClient->GuiPoll(); // Convert from net client context to GUI script context return guiManager->GetScriptInterface().CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), poll.get()); } void AssignNetworkPlayer(void* UNUSED(cbdata), int playerID, std::string guid) { ENSURE(g_NetServer); g_NetServer->AssignPlayer(playerID, guid); } void SendNetworkChat(void* UNUSED(cbdata), std::wstring message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } std::vector GetAIs(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); return ICmpAIManager::GetAIs(guiManager->GetScriptInterface()); } std::vector GetSavedGames(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); return SavedGames::GetSavedGames(guiManager->GetScriptInterface()); } bool DeleteSavedGame(void* UNUSED(cbdata), std::wstring name) { return SavedGames::DeleteSavedGame(name); } void OpenURL(void* UNUSED(cbdata), std::string url) { sys_open_url(url); } void RestartInAtlas(void* UNUSED(cbdata)) { restart_mainloop_in_atlas(); } bool AtlasIsAvailable(void* UNUSED(cbdata)) { return ATLAS_IsAvailable(); } CScriptVal LoadMapSettings(void* cbdata, VfsPath pathname) { CGUIManager* guiManager = static_cast (cbdata); CMapSummaryReader reader; if (reader.LoadMap(pathname.ChangeExtension(L".xml")) != PSRETURN_OK) return CScriptVal(); return reader.GetMapSettings(guiManager->GetScriptInterface()).get(); } CScriptVal GetMapSettings(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return CScriptVal(); return guiManager->GetScriptInterface().CloneValueFromOtherContext( g_Game->GetSimulation2()->GetScriptInterface(), g_Game->GetSimulation2()->GetMapSettings().get()); } /** * Start / stop camera following mode * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollow(void* UNUSED(cbdata), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, false); } /** * Start / stop first-person camera following mode * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollowFPS(void* UNUSED(cbdata), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, true); } /// Move camera to a 2D location void CameraMoveTo(void* UNUSED(cbdata), entity_pos_t x, entity_pos_t z) { // called from JS; must not fail if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain())) return; CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; target.X = x.ToFloat(); target.Z = z.ToFloat(); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } entity_id_t GetFollowedEntity(void* UNUSED(cbdata)) { if (g_Game && g_Game->GetView()) return g_Game->GetView()->GetFollowedEntity(); return INVALID_ENTITY; } bool HotkeyIsPressed_(void* UNUSED(cbdata), std::string hotkeyName) { return HotkeyIsPressed(hotkeyName); } void DisplayErrorDialog(void* UNUSED(cbdata), std::wstring msg) { debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL); } CScriptVal GetProfilerState(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); return g_ProfileViewer.SaveToJS(guiManager->GetScriptInterface()); } bool IsUserReportEnabled(void* UNUSED(cbdata)) { return g_UserReporter.IsReportingEnabled(); } void SetUserReportEnabled(void* UNUSED(cbdata), bool enabled) { g_UserReporter.SetReportingEnabled(enabled); } std::string GetUserReportStatus(void* UNUSED(cbdata)) { return g_UserReporter.GetStatus(); } void SubmitUserReport(void* UNUSED(cbdata), std::string type, int version, std::wstring data) { g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data)); } void SetSimRate(void* UNUSED(cbdata), float rate) { g_Game->SetSimRate(rate); } void SetTurnLength(void* UNUSED(cbdata), int length) { if (g_NetServer) g_NetServer->SetTurnLength(length); else LOGERROR(L"Only network host can change turn length"); } // Focus the game camera on a given position. void SetCameraTarget(void* UNUSED(cbdata), float x, float y, float z) { g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z)); } // Deliberately cause the game to crash. // Currently implemented via access violation (read of address 0). // Useful for testing the crashlog/stack trace code. int Crash(void* UNUSED(cbdata)) { debug_printf(L"Crashing at user's request.\n"); return *(volatile int*)0; } void DebugWarn(void* UNUSED(cbdata)) { debug_warn(L"Warning at user's request."); } // Force a JS garbage collection cycle to take place immediately. // Writes an indication of how long this took to the console. void ForceGC(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); double time = timer_Time(); JS_GC(guiManager->GetScriptInterface().GetContext()); time = timer_Time() - time; g_Console->InsertMessage(L"Garbage collection completed in: %f", time); } void DumpSimState(void* UNUSED(cbdata)) { OsPath path = psLogDir()/"sim_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); g_Game->GetSimulation2()->DumpDebugState(file); } +void DumpTerrainMipmap(void* UNUSED(cbdata)) +{ + VfsPath filename(L"screenshots/terrainmipmap.png"); + g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename); + OsPath realPath; + g_VFS->GetRealPath(filename, realPath); + LOGMESSAGERENDER(L"Terrain mipmap written to '%ls'", realPath.string().c_str()); +} + void EnableTimeWarpRecording(void* UNUSED(cbdata), unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void RewindTimeWarp(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void QuickSave(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->QuickSave(); } void QuickLoad(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->QuickLoad(); } void SetBoundingBoxDebugOverlay(void* UNUSED(cbdata), bool enabled) { ICmpSelectable::ms_EnableDebugOverlays = enabled; } } // namespace void GuiScriptingInit(ScriptInterface& scriptInterface) { // GUI manager functions: scriptInterface.RegisterFunction("GetActiveGui"); scriptInterface.RegisterFunction("PushGuiPage"); scriptInterface.RegisterFunction("SwitchGuiPage"); scriptInterface.RegisterFunction("PopGuiPage"); // Simulation<->GUI interface functions: scriptInterface.RegisterFunction("GuiInterfaceCall"); scriptInterface.RegisterFunction("PostNetworkCommand"); // Entity picking scriptInterface.RegisterFunction, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint"); scriptInterface.RegisterFunction, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect"); scriptInterface.RegisterFunction, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen"); scriptInterface.RegisterFunction, std::string, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities"); scriptInterface.RegisterFunction("GetTerrainAtPoint"); // Network / game setup functions scriptInterface.RegisterFunction("StartNetworkGame"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("StartNetworkHost"); scriptInterface.RegisterFunction("StartNetworkJoin"); scriptInterface.RegisterFunction("DisconnectNetworkGame"); scriptInterface.RegisterFunction("PollNetworkClient"); scriptInterface.RegisterFunction("SetNetworkGameAttributes"); scriptInterface.RegisterFunction("AssignNetworkPlayer"); scriptInterface.RegisterFunction("SendNetworkChat"); scriptInterface.RegisterFunction, &GetAIs>("GetAIs"); // Saved games scriptInterface.RegisterFunction("StartSavedGame"); scriptInterface.RegisterFunction, &GetSavedGames>("GetSavedGames"); scriptInterface.RegisterFunction("DeleteSavedGame"); scriptInterface.RegisterFunction("SaveGame"); scriptInterface.RegisterFunction("QuickSave"); scriptInterface.RegisterFunction("QuickLoad"); // Misc functions scriptInterface.RegisterFunction("SetCursor"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("GetDefaultPlayerName"); scriptInterface.RegisterFunction("OpenURL"); scriptInterface.RegisterFunction("RestartInAtlas"); scriptInterface.RegisterFunction("AtlasIsAvailable"); scriptInterface.RegisterFunction("LoadMapSettings"); scriptInterface.RegisterFunction("GetMapSettings"); scriptInterface.RegisterFunction("CameraFollow"); scriptInterface.RegisterFunction("CameraFollowFPS"); scriptInterface.RegisterFunction("CameraMoveTo"); scriptInterface.RegisterFunction("GetFollowedEntity"); scriptInterface.RegisterFunction("HotkeyIsPressed"); scriptInterface.RegisterFunction("DisplayErrorDialog"); scriptInterface.RegisterFunction("GetProfilerState"); // User report functions scriptInterface.RegisterFunction("IsUserReportEnabled"); scriptInterface.RegisterFunction("SetUserReportEnabled"); scriptInterface.RegisterFunction("GetUserReportStatus"); scriptInterface.RegisterFunction("SubmitUserReport"); // Development/debugging functions scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("SetTurnLength"); scriptInterface.RegisterFunction("SetCameraTarget"); scriptInterface.RegisterFunction("Crash"); scriptInterface.RegisterFunction("DebugWarn"); scriptInterface.RegisterFunction("ForceGC"); scriptInterface.RegisterFunction("DumpSimState"); + scriptInterface.RegisterFunction("DumpTerrainMipmap"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); } Index: ps/trunk/source/graphics/Terrain.cpp =================================================================== --- ps/trunk/source/graphics/Terrain.cpp (revision 11555) +++ ps/trunk/source/graphics/Terrain.cpp (revision 11556) @@ -1,633 +1,664 @@ /* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Describes ground via heightmap and array of CPatch. */ #include "precompiled.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/sysdep/cpu.h" #include "renderer/Renderer.h" #include "TerrainProperties.h" #include "TerrainTextureEntry.h" #include "TerrainTextureManager.h" #include #include "Terrain.h" #include "Patch.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::CTerrain() : m_Heightmap(0), m_Patches(0), m_MapSize(0), m_MapSizePatches(0), m_BaseColour(255, 255, 255, 255) { } /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::~CTerrain() { ReleaseData(); } /////////////////////////////////////////////////////////////////////////////// // ReleaseData: delete any data allocated by this terrain void CTerrain::ReleaseData() { + m_HeightMipmap.ReleaseData(); + delete[] m_Heightmap; delete[] m_Patches; } /////////////////////////////////////////////////////////////////////////////// // Initialise: initialise this terrain to the given size // using given heightmap to setup elevation data -bool CTerrain::Initialize(ssize_t patchesPerSide,const u16* data) +bool CTerrain::Initialize(ssize_t patchesPerSide, const u16* data) { // clean up any previous terrain ReleaseData(); // store terrain size - m_MapSize=patchesPerSide*PATCH_SIZE+1; - m_MapSizePatches=patchesPerSide; + m_MapSize = patchesPerSide*PATCH_SIZE+1; + m_MapSizePatches = patchesPerSide; // allocate data for new terrain - m_Heightmap=new u16[m_MapSize*m_MapSize]; - m_Patches=new CPatch[m_MapSizePatches*m_MapSizePatches]; + m_Heightmap = new u16[m_MapSize*m_MapSize]; + m_Patches = new CPatch[m_MapSizePatches*m_MapSizePatches]; // given a heightmap? - if (data) { + if (data) + { // yes; keep a copy of it - memcpy(m_Heightmap,data,m_MapSize*m_MapSize*sizeof(u16)); - } else { + memcpy(m_Heightmap, data, m_MapSize*m_MapSize*sizeof(u16)); + } + else + { // build a flat terrain - memset(m_Heightmap,0,m_MapSize*m_MapSize*sizeof(u16)); + memset(m_Heightmap, 0, m_MapSize*m_MapSize*sizeof(u16)); } // setup patch parents, indices etc InitialisePatches(); + // initialise mipmap + m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); + return true; } /////////////////////////////////////////////////////////////////////////////// CStr8 CTerrain::GetMovementClass(ssize_t i, ssize_t j) const { CMiniPatch* tile = GetTile(i, j); if (tile && tile->GetTextureEntry()) return tile->GetTextureEntry()->GetProperties().GetMovementClass(); return "default"; } /////////////////////////////////////////////////////////////////////////////// // CalcPosition: calculate the world space position of the vertex at (i,j) // If i,j is off the map, it acts as if the edges of the terrain are extended // outwards to infinity void CTerrain::CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const { ssize_t hi = clamp(i, (ssize_t)0, m_MapSize-1); ssize_t hj = clamp(j, (ssize_t)0, m_MapSize-1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = float(i*TERRAIN_TILE_SIZE); pos.Y = float(height*HEIGHT_SCALE); pos.Z = float(j*TERRAIN_TILE_SIZE); } /////////////////////////////////////////////////////////////////////////////// // CalcPositionFixed: calculate the world space position of the vertex at (i,j) void CTerrain::CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const { ssize_t hi = clamp(i, (ssize_t)0, m_MapSize-1); ssize_t hj = clamp(j, (ssize_t)0, m_MapSize-1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = fixed::FromInt(i) * (int)TERRAIN_TILE_SIZE; pos.Y = fixed::FromInt(height) / (int)HEIGHT_UNITS_PER_METRE; pos.Z = fixed::FromInt(j) * (int)TERRAIN_TILE_SIZE; } /////////////////////////////////////////////////////////////////////////////// // CalcNormal: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const { CVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CVector3D basepos; CalcPosition(i, j, basepos); if (i > 0) { CalcPosition(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPosition(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPosition(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPosition(i, j+1, down); down -= basepos; down.Normalize(); } CVector3D n0 = up.Cross(left); CVector3D n1 = left.Cross(down); CVector3D n2 = down.Cross(right); CVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; float nlen=normal.Length(); if (nlen>0.00001f) normal*=1.0f/nlen; } /////////////////////////////////////////////////////////////////////////////// // CalcNormalFixed: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const { CFixedVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CFixedVector3D basepos; CalcPositionFixed(i, j, basepos); if (i > 0) { CalcPositionFixed(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPositionFixed(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPositionFixed(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPositionFixed(i, j+1, down); down -= basepos; down.Normalize(); } CFixedVector3D n0 = up.Cross(left); CFixedVector3D n1 = left.Cross(down); CFixedVector3D n2 = down.Cross(right); CFixedVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; normal.Normalize(); } CVector3D CTerrain::CalcExactNormal(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = clamp((ssize_t)floor(x/TERRAIN_TILE_SIZE), (ssize_t)0, m_MapSize-2); const ssize_t zi = clamp((ssize_t)floor(z/TERRAIN_TILE_SIZE), (ssize_t)0, m_MapSize-2); const float xf = clamp(x/TERRAIN_TILE_SIZE-xi, 0.0f, 1.0f); const float zf = clamp(z/TERRAIN_TILE_SIZE-zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the normal of that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Upper-right triangle (don't use h00) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Lower-right triangle (don't use h01) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } } /////////////////////////////////////////////////////////////////////////////// // GetPatch: return the patch at (i,j) in patch space, or null if the patch is // out of bounds CPatch* CTerrain::GetPatch(ssize_t i, ssize_t j) const { // range check (invalid indices are passed in by the culling and // patch blend code because they iterate from 0..#patches and examine // neighbors without checking if they're already on the edge) if( (size_t)i >= (size_t)m_MapSizePatches || (size_t)j >= (size_t)m_MapSizePatches ) return 0; return &m_Patches[(j*m_MapSizePatches)+i]; } /////////////////////////////////////////////////////////////////////////////// // GetTile: return the tile at (i,j) in tile space, or null if the tile is out // of bounds CMiniPatch* CTerrain::GetTile(ssize_t i, ssize_t j) const { // see comment above if( (size_t)i >= (size_t)(m_MapSize-1) || (size_t)j >= (size_t)(m_MapSize-1) ) return 0; CPatch* patch=GetPatch(i/PATCH_SIZE, j/PATCH_SIZE); // can't fail (due to above check) return &patch->m_MiniPatches[j%PATCH_SIZE][i%PATCH_SIZE]; } float CTerrain::GetVertexGroundLevel(ssize_t i, ssize_t j) const { i = clamp(i, (ssize_t)0, m_MapSize-1); j = clamp(j, (ssize_t)0, m_MapSize-1); return HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i]; } fixed CTerrain::GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const { i = clamp(i, (ssize_t)0, m_MapSize-1); j = clamp(j, (ssize_t)0, m_MapSize-1); // Convert to fixed metres (being careful to avoid intermediate overflows) return fixed::FromInt(m_Heightmap[j*m_MapSize + i] / 2) / (int)(HEIGHT_UNITS_PER_METRE / 2); } fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = clamp(i, (ssize_t)0, m_MapSize-2); j = clamp(j, (ssize_t)0, m_MapSize-2); u16 h00 = m_Heightmap[j*m_MapSize + i]; u16 h01 = m_Heightmap[(j+1)*m_MapSize + i]; u16 h10 = m_Heightmap[j*m_MapSize + (i+1)]; u16 h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Difference of highest point from lowest point u16 delta = std::max(std::max(h00, h01), std::max(h10, h11)) - std::min(std::min(h00, h01), std::min(h10, h11)); // Compute fractional slope (being careful to avoid intermediate overflows) return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE; } +float CTerrain::GetFilteredGroundLevel(float x, float z, float radius) const +{ + // convert to [0,1] interval + float nx = x / (TERRAIN_TILE_SIZE*m_MapSize); + float nz = z / (TERRAIN_TILE_SIZE*m_MapSize); + float nr = radius / (TERRAIN_TILE_SIZE*m_MapSize); + + // get trilinear filtered mipmap height + return HEIGHT_SCALE * m_HeightMipmap.GetTrilinearGroundLevel(nx, nz, nr); +} + float CTerrain::GetExactGroundLevel(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = clamp((ssize_t)floor(x/TERRAIN_TILE_SIZE), (ssize_t)0, m_MapSize-2); const ssize_t zi = clamp((ssize_t)floor(z/TERRAIN_TILE_SIZE), (ssize_t)0, m_MapSize-2); const float xf = clamp(x/TERRAIN_TILE_SIZE-xi, 0.0f, 1.0f); const float zf = clamp(z/TERRAIN_TILE_SIZE-zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the linearly-interpolated height on that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h01-h00)*zf); } else { // Upper-right triangle (don't use h00) return HEIGHT_SCALE * (h11 + (h01-h11)*(1-xf) + (h10-h11)*(1-zf)); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return HEIGHT_SCALE * (h00 + (h11-h01)*xf + (h01-h00)*zf); } else { // Lower-right triangle (don't use h01) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h11-h10)*zf); } } } fixed CTerrain::GetExactGroundLevelFixed(fixed x, fixed z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = clamp((ssize_t)(x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2); const ssize_t zi = clamp((ssize_t)(z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), (ssize_t)0, m_MapSize-2); const fixed one = fixed::FromInt(1); const fixed xf = clamp((x / (int)TERRAIN_TILE_SIZE) - fixed::FromInt(xi), fixed::Zero(), one); const fixed zf = clamp((z / (int)TERRAIN_TILE_SIZE) - fixed::FromInt(zi), fixed::Zero(), one); u16 h00 = m_Heightmap[zi*m_MapSize + xi]; u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Intermediate scaling of xf, so we don't overflow in the multiplications below // (h00 <= 65535, xf <= 1, max fixed is < 32768; divide by 2 here so xf1*h00 <= 32767.5) const fixed xf0 = xf / 2; const fixed xf1 = (one - xf) / 2; // Linearly interpolate return ((one - zf).Multiply(xf1 * h00 + xf0 * h10) + zf.Multiply(xf1 * h01 + xf0 * h11)) / (int)(HEIGHT_UNITS_PER_METRE / 2); // TODO: This should probably be more like GetExactGroundLevel() // in handling triangulation properly } bool CTerrain::GetTriangulationDir(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = clamp(i, (ssize_t)0, m_MapSize-2); j = clamp(j, (ssize_t)0, m_MapSize-2); int h00 = m_Heightmap[j*m_MapSize + i]; int h01 = m_Heightmap[(j+1)*m_MapSize + i]; int h10 = m_Heightmap[j*m_MapSize + (i+1)]; int h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Prefer triangulating in whichever direction means the midpoint of the diagonal // will be the highest. (In particular this means a diagonal edge will be straight // along the top, and jagged along the bottom, which makes sense for terrain.) int mid1 = h00+h11; int mid2 = h01+h10; return (mid1 < mid2); } /////////////////////////////////////////////////////////////////////////////// // Resize: resize this terrain to the given size (in patches per side) void CTerrain::Resize(ssize_t size) { if (size==m_MapSizePatches) { // inexplicable request to resize terrain to the same size .. ignore it return; } if (!m_Heightmap) { // not yet created a terrain; build a default terrain of the given size now Initialize(size,0); return; } // allocate data for new terrain ssize_t newMapSize=size*PATCH_SIZE+1; u16* newHeightmap=new u16[newMapSize*newMapSize]; CPatch* newPatches=new CPatch[size*size]; if (size>m_MapSizePatches) { // new map is bigger than old one - zero the heightmap so we don't get uninitialised // height data along the expanded edges memset(newHeightmap,0,newMapSize*newMapSize*sizeof(u16)); } // now copy over rows of data u16* src=m_Heightmap; u16* dst=newHeightmap; ssize_t copysize=std::min(newMapSize, m_MapSize); for (ssize_t j=0;jm_MapSize) { // extend the last height to the end of the row for (size_t i=0;im_MapSize) { // copy over heights of the last row to any remaining rows src=newHeightmap+((m_MapSize-1)*newMapSize); dst=src+newMapSize; for (ssize_t i=0;im_MapSizePatches) { // copy over the last tile from each column for (ssize_t n=0;nm_MapSizePatches) { // copy over the last tile from each column CPatch* srcpatch=&newPatches[(m_MapSizePatches-1)*size]; CPatch* dstpatch=srcpatch+size; for (ssize_t p=0;p<(ssize_t)size-m_MapSizePatches;p++) { for (ssize_t n=0;n<(ssize_t)size;n++) { for (ssize_t m=0;mm_MiniPatches[15][k]; CMiniPatch& dst=dstpatch->m_MiniPatches[m][k]; dst = src; } } srcpatch++; dstpatch++; } } } // release all the original data ReleaseData(); // store new data m_Heightmap=newHeightmap; m_Patches=newPatches; m_MapSize=(ssize_t)newMapSize; m_MapSizePatches=(ssize_t)size; // initialise all the new patches InitialisePatches(); + + // initialise mipmap + m_HeightMipmap.Initialize(m_MapSize,m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// // InitialisePatches: initialise patch data void CTerrain::InitialisePatches() { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->Initialize(this, i, j); } } } /////////////////////////////////////////////////////////////////////////////// // SetHeightMap: set up a new heightmap from 16-bit source data; // assumes heightmap matches current terrain size void CTerrain::SetHeightMap(u16* heightmap) { // keep a copy of the given heightmap memcpy(m_Heightmap, heightmap, m_MapSize*m_MapSize*sizeof(u16)); // recalculate patch bounds, invalidate vertices for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->InvalidateBounds(); patch->SetDirty(RENDERDATA_UPDATE_VERTICES); } } + + // update mipmap + m_HeightMipmap.Update(m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// void CTerrain::MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags) { // Finds the inclusive limits of the patches that include the specified range of tiles ssize_t pi0 = clamp( i0 /PATCH_SIZE, (ssize_t)0, m_MapSizePatches-1); ssize_t pi1 = clamp((i1-1)/PATCH_SIZE, (ssize_t)0, m_MapSizePatches-1); ssize_t pj0 = clamp( j0 /PATCH_SIZE, (ssize_t)0, m_MapSizePatches-1); ssize_t pj1 = clamp((j1-1)/PATCH_SIZE, (ssize_t)0, m_MapSizePatches-1); for (ssize_t j = pj0; j <= pj1; j++) { for (ssize_t i = pi0; i <= pi1; i++) { CPatch* patch = GetPatch(i, j); // can't fail (i,j were clamped) if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } + + if (m_Heightmap) + m_HeightMipmap.Update(m_Heightmap, i0, j0, i1, j1); } void CTerrain::MakeDirty(int dirtyFlags) { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } + + if (m_Heightmap) + m_HeightMipmap.Update(m_Heightmap); } CBoundingBoxAligned CTerrain::GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) { i0 = clamp(i0, (ssize_t)0, m_MapSize-1); j0 = clamp(j0, (ssize_t)0, m_MapSize-1); i1 = clamp(i1, (ssize_t)0, m_MapSize-1); j1 = clamp(j1, (ssize_t)0, m_MapSize-1); u16 minH = 65535; u16 maxH = 0; for (ssize_t j = j0; j <= j1; ++j) { for (ssize_t i = i0; i <= i1; ++i) { minH = std::min(minH, m_Heightmap[j*m_MapSize + i]); maxH = std::max(maxH, m_Heightmap[j*m_MapSize + i]); } } CBoundingBoxAligned bound; bound[0].X = (float)(i0*TERRAIN_TILE_SIZE); bound[0].Y = (float)(minH*HEIGHT_SCALE); bound[0].Z = (float)(j0*TERRAIN_TILE_SIZE); bound[1].X = (float)(i1*TERRAIN_TILE_SIZE); bound[1].Y = (float)(maxH*HEIGHT_SCALE); bound[1].Z = (float)(j1*TERRAIN_TILE_SIZE); return bound; } Index: ps/trunk/source/graphics/HeightMipmap.cpp =================================================================== --- ps/trunk/source/graphics/HeightMipmap.cpp (nonexistent) +++ ps/trunk/source/graphics/HeightMipmap.cpp (revision 11556) @@ -0,0 +1,261 @@ +/* Copyright (C) 2012 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 "HeightMipmap.h" + +#include "lib/bits.h" +#include "lib/timer.h" +#include "lib/allocators/shared_ptr.h" +#include "lib/tex/tex.h" +#include "maths/MathUtil.h" +#include "ps/Filesystem.h" + +#include + +CHeightMipmap::CHeightMipmap() +{ +} + +CHeightMipmap::~CHeightMipmap() +{ + ReleaseData(); +} + +void CHeightMipmap::ReleaseData() +{ + for (size_t i = 0; i < m_Mipmap.size(); ++i) + { + delete[] m_Mipmap[i].m_Heightmap; + m_Mipmap[i].m_MapSize = 0; + } + m_Mipmap.clear(); +} + +void CHeightMipmap::Update(const u16* ptr) +{ + ENSURE(ptr != 0); + + Update(ptr, 0, 0, m_MapSize, m_MapSize); +} + +void CHeightMipmap::Update(const u16* ptr, size_t left, size_t bottom, size_t right, size_t top) +{ + ENSURE(ptr != 0); + + size_t mapSize = m_MapSize; + + for (size_t i = 0; i < m_Mipmap.size(); ++i) + { + // update window + left = clamp((size_t)floorf((float)left / mapSize * m_Mipmap[i].m_MapSize), 0, m_Mipmap[i].m_MapSize - 1); + bottom = clamp((size_t)floorf((float)bottom / mapSize * m_Mipmap[i].m_MapSize), 0, m_Mipmap[i].m_MapSize - 1); + + right = clamp((size_t)ceilf((float)right / mapSize * m_Mipmap[i].m_MapSize), 0, m_Mipmap[i].m_MapSize); + top = clamp((size_t)ceilf((float)top / mapSize * m_Mipmap[i].m_MapSize), 0, m_Mipmap[i].m_MapSize); + + // TODO: should verify that the bounds calculations are actually correct + + // update mipmap + BilinearUpdate(m_Mipmap[i], mapSize, ptr, left, bottom, right, top); + + mapSize = m_Mipmap[i].m_MapSize; + ptr = m_Mipmap[i].m_Heightmap; + } +} + +void CHeightMipmap::Initialize(size_t mapSize, const u16* ptr) +{ + ENSURE(ptr != 0); + ENSURE(mapSize > 0); + + ReleaseData(); + + m_MapSize = mapSize; + size_t mipmapSize = round_down_to_pow2(mapSize); + + while (mipmapSize > 1) + { + m_Mipmap.push_back(SMipmap(mipmapSize, new u16[mipmapSize*mipmapSize])); + mipmapSize >>= 1; + }; + + Update(ptr); +} + +float CHeightMipmap::GetTrilinearGroundLevel(float x, float z, float radius) const +{ + float y; + if (radius <= 0.0f) // avoid logf of non-positive value + y = 0.0f; + else + y = clamp(logf(radius * m_Mipmap[0].m_MapSize) / logf(2), 0, m_Mipmap.size()); + + const size_t iy = (size_t)clamp((ssize_t)floorf(y), 0, m_Mipmap.size() - 2); + + const float fy = y - iy; + + const float h0 = BilinearFilter(m_Mipmap[iy], x, z); + const float h1 = BilinearFilter(m_Mipmap[iy + 1], x, z); + + return (1 - fy) * h0 + fy * h1; +} + +float CHeightMipmap::BilinearFilter(const SMipmap &mipmap, float x, float z) const +{ + x *= mipmap.m_MapSize; + z *= mipmap.m_MapSize; + + const size_t xi = (size_t)clamp((ssize_t)floor(x), 0, mipmap.m_MapSize - 2); + const size_t zi = (size_t)clamp((ssize_t)floor(z), 0, mipmap.m_MapSize - 2); + + const float xf = clamp(x-xi, 0.0f, 1.0f); + const float zf = clamp(z-zi, 0.0f, 1.0f); + + const float h00 = mipmap.m_Heightmap[zi*mipmap.m_MapSize + xi]; + const float h01 = mipmap.m_Heightmap[(zi+1)*mipmap.m_MapSize + xi]; + const float h10 = mipmap.m_Heightmap[zi*mipmap.m_MapSize + (xi+1)]; + const float h11 = mipmap.m_Heightmap[(zi+1)*mipmap.m_MapSize + (xi+1)]; + + return + (1.f - xf) * (1.f - zf) * h00 + + xf * (1.f - zf) * h10 + + (1.f - xf) * zf * h01 + + xf * zf * h11; +} + +void CHeightMipmap::HalfResizeUpdate(SMipmap &out_mipmap, size_t mapSize, const u16* ptr, size_t left, size_t bottom, size_t right, size_t top) +{ + // specialized, faster version of BilinearUpdate for powers of 2 + + ENSURE(out_mipmap.m_MapSize != 0); + + if (out_mipmap.m_MapSize * 2 != mapSize) + debug_warn(L"wrong size"); + + // valid update window + ENSURE(left < out_mipmap.m_MapSize); + ENSURE(bottom < out_mipmap.m_MapSize); + ENSURE(right > left && right <= out_mipmap.m_MapSize); + ENSURE(top > bottom && top <= out_mipmap.m_MapSize); + + for (size_t dstZ = bottom; dstZ < top; ++dstZ) + { + for (size_t dstX = left; dstX < right; ++dstX) + { + size_t srcX = dstX << 1; + size_t srcZ = dstZ << 1; + + u16 h00 = ptr[srcX + 0 + srcZ * mapSize]; + u16 h10 = ptr[srcX + 1 + srcZ * mapSize]; + u16 h01 = ptr[srcX + 0 + (srcZ + 1) * mapSize]; + u16 h11 = ptr[srcX + 1 + (srcZ + 1) * mapSize]; + + out_mipmap.m_Heightmap[dstX + dstZ * out_mipmap.m_MapSize] = (h00 + h10 + h01 + h11) / 4; + } + } +} + +void CHeightMipmap::BilinearUpdate(SMipmap &out_mipmap, size_t mapSize, const u16* ptr, size_t left, size_t bottom, size_t right, size_t top) +{ + ENSURE(out_mipmap.m_MapSize != 0); + + // filter should have full coverage + ENSURE(out_mipmap.m_MapSize <= mapSize && out_mipmap.m_MapSize * 2 >= mapSize); + + // valid update window + ENSURE(left < out_mipmap.m_MapSize); + ENSURE(bottom < out_mipmap.m_MapSize); + ENSURE(right > left && right <= out_mipmap.m_MapSize); + ENSURE(top > bottom && top <= out_mipmap.m_MapSize); + + if (out_mipmap.m_MapSize * 2 == mapSize) + { + // optimized for powers of 2 + HalfResizeUpdate(out_mipmap, mapSize, ptr, left, bottom, right, top); + } + else + { + for (size_t dstZ = bottom; dstZ < top; ++dstZ) + { + for (size_t dstX = left; dstX < right; ++dstX) + { + const float x = ((float)dstX / (float)out_mipmap.m_MapSize) * mapSize; + const float z = ((float)dstZ / (float)out_mipmap.m_MapSize) * mapSize; + + const size_t srcX = clamp((size_t)x, 0, mapSize - 2); + const size_t srcZ = clamp((size_t)z, 0, mapSize - 2); + + const float fx = clamp(x - srcX, 0.0f, 1.0f); + const float fz = clamp(z - srcZ, 0.0f, 1.0f); + + const float h00 = ptr[srcX + 0 + srcZ * mapSize]; + const float h10 = ptr[srcX + 1 + srcZ * mapSize]; + const float h01 = ptr[srcX + 0 + (srcZ + 1) * mapSize]; + const float h11 = ptr[srcX + 1 + (srcZ + 1) * mapSize]; + + out_mipmap.m_Heightmap[dstX + dstZ * out_mipmap.m_MapSize] = (u16) + ((1.f - fx) * (1.f - fz) * h00 + + fx * (1.f - fz) * h10 + + (1.f - fx) * fz * h01 + + fx * fz * h11); + } + } + } +} + +void CHeightMipmap::DumpToDisk(const VfsPath& filename) const +{ + const size_t w = m_MapSize; + const size_t h = m_MapSize * 2; + const size_t bpp = 8; + int flags = TEX_GREY|TEX_TOP_DOWN; + + const size_t img_size = w * h * bpp/8; + const size_t hdr_size = tex_hdr_size(filename); + shared_ptr buf; + AllocateAligned(buf, hdr_size+img_size, maxSectorSize); + void* img = buf.get() + hdr_size; + Tex t; + WARN_IF_ERR(tex_wrap(w, h, bpp, flags, buf, hdr_size, &t)); + + memset(img, 0x00, img_size); + size_t yoff = 0; + for (size_t i = 0; i < m_Mipmap.size(); ++i) + { + size_t size = m_Mipmap[i].m_MapSize; + u16* heightmap = m_Mipmap[i].m_Heightmap; + ENSURE(size+yoff <= h); + for (size_t y = 0; y < size; ++y) + { + for (size_t x = 0; x < size; ++x) + { + u16 val = heightmap[x + y*size]; + ((u8*)img)[x + (y+yoff)*w] = val >> 8; + } + } + yoff += size; + } + + DynArray da; + WARN_IF_ERR(tex_encode(&t, filename.Extension(), &da)); + g_VFS->CreateFile(filename, DummySharedPtr(da.base), da.pos); + (void)da_free(&da); + + tex_free(&t); +} Index: ps/trunk/source/graphics/GameView.cpp =================================================================== --- ps/trunk/source/graphics/GameView.cpp (revision 11555) +++ ps/trunk/source/graphics/GameView.cpp (revision 11556) @@ -1,1078 +1,1120 @@ /* Copyright (C) 2012 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/Camera.h" #include "graphics/CinemaTrack.h" #include "graphics/ColladaManager.h" #include "graphics/HFTracer.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Model.h" #include "graphics/ObjectManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.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 "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/Joystick.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "scripting/ScriptableObject.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.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; /** * A value with exponential decay towards the target value. */ class CSmoothedValue { public: CSmoothedValue(float value, float smoothness, float minDelta) : m_Target(value), m_Current(value), m_Smoothness(smoothness), m_MinDelta(minDelta) { } float GetSmoothedValue() { return m_Current; } void SetValueSmoothly(float value) { m_Target = value; } void AddSmoothly(float value) { m_Target += value; } void Add(float value) { m_Target += value; m_Current += value; } float GetValue() { return m_Target; } void SetValue(float value) { m_Target = value; m_Current = value; } float Update(float time) { if (fabs(m_Target - m_Current) < m_MinDelta) return 0.0f; double p = pow((double)m_Smoothness, 10.0 * (double)time); // (add the factor of 10 so that smoothnesses don't have to be tiny numbers) double delta = (m_Target - m_Current) * (1.0 - p); m_Current += delta; return (float)delta; } void ClampSmoothly(float min, float max) { m_Target = Clamp(m_Target, (double)min, (double)max); } // Wrap so 'target' is in the range [min, max] void Wrap(float min, float max) { double t = fmod(m_Target - min, (double)(max - min)); if (t < 0) t += max - min; t += min; m_Current += t - m_Target; m_Target = t; } private: double m_Target; // the value which m_Current is tending towards double m_Current; // (We use double because the extra precision is worthwhile here) float m_MinDelta; // cutoff where we stop moving (to avoid ugly shimmering effects) public: float m_Smoothness; }; class CGameViewImpl : public CJSObject { NONCOPYABLE(CGameViewImpl); public: CGameViewImpl(CGame* game) : Game(game), ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()), LOSTexture(*game->GetSimulation2()), TerritoryTexture(*game->GetSimulation2()), ViewCamera(), CullCamera(), LockCullCamera(false), ConstrainCamera(true), Culling(true), FollowEntity(INVALID_ENTITY), FollowFirstPerson(false), // Dummy values (these will be filled in by the config file) ViewScrollSpeed(0), ViewRotateXSpeed(0), ViewRotateXMin(0), ViewRotateXMax(0), ViewRotateXDefault(0), ViewRotateYSpeed(0), ViewRotateYSpeedWheel(0), ViewRotateYDefault(0), ViewDragSpeed(0), ViewZoomSpeed(0), ViewZoomSpeedWheel(0), ViewZoomMin(0), ViewZoomMax(0), ViewZoomDefault(0), ViewFOV(DEGTORAD(45.f)), ViewNear(2.f), ViewFar(4096.f), JoystickPanX(-1), JoystickPanY(-1), JoystickRotateX(-1), JoystickRotateY(-1), JoystickZoomIn(-1), JoystickZoomOut(-1), + HeightSmoothness(0.5f), + HeightMin(16.f), PosX(0, 0, 0.01f), PosY(0, 0, 0.01f), PosZ(0, 0, 0.01f), Zoom(0, 0, 0.1f), RotateX(0, 0, 0.001f), RotateY(0, 0, 0.001f) { } CGame* Game; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; /** * 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; /** * Whether the camera movement should be constrained by min/max limits * and terrain avoidance. */ bool ConstrainCamera; /** * Cache global lighting environment. This is used to check whether the * environment has changed during the last frame, so that vertex data can be updated etc. */ CLightEnv CachedLightEnv; CCinemaManager TrackManager; /** * Entity for the camera to follow, or INVALID_ENTITY if none. */ entity_id_t FollowEntity; /** * Whether to follow FollowEntity in first-person mode. */ bool FollowFirstPerson; //////////////////////////////////////// // Settings float ViewScrollSpeed; float ViewRotateXSpeed; float ViewRotateXMin; float ViewRotateXMax; float ViewRotateXDefault; float ViewRotateYSpeed; float ViewRotateYSpeedWheel; float ViewRotateYDefault; float ViewDragSpeed; float ViewZoomSpeed; float ViewZoomSpeedWheel; float ViewZoomMin; float ViewZoomMax; float ViewZoomDefault; float ViewFOV; float ViewNear; float ViewFar; int JoystickPanX; int JoystickPanY; int JoystickRotateX; int JoystickRotateY; int JoystickZoomIn; int JoystickZoomOut; + float HeightSmoothness; + float HeightMin; //////////////////////////////////////// // Camera Controls State CSmoothedValue PosX; CSmoothedValue PosY; CSmoothedValue PosZ; CSmoothedValue Zoom; CSmoothedValue RotateX; // inclination around x axis (relative to camera) CSmoothedValue RotateY; // rotation around y (vertical) axis static void ScriptingInit(); }; static void SetupCameraMatrixSmooth(CGameViewImpl* m, 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()); } static void SetupCameraMatrixSmoothRot(CGameViewImpl* m, 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()); } static void SetupCameraMatrixNonSmooth(CGameViewImpl* m, 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()); } CGameView::CGameView(CGame *pGame): m(new CGameViewImpl(pGame)) { SViewPort vp; vp.m_X=0; vp.m_Y=0; vp.m_Width=g_xres; vp.m_Height=g_yres; m->ViewCamera.SetViewPort(vp); m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); m->CullCamera = m->ViewCamera; g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); } CGameView::~CGameView() { UnloadResources(); delete m; } void CGameView::SetViewport(const SViewPort& vp) { m->ViewCamera.SetViewPort(vp); m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); } CObjectManager& CGameView::GetObjectManager() const { return m->ObjectManager; } JSObject* CGameView::GetScript() { return m->GetScript(); } /*static*/ void CGameView::ScriptingInit() { return CGameViewImpl::ScriptingInit(); } CCamera* CGameView::GetCamera() { return &m->ViewCamera; } CCinemaManager* CGameView::GetCinema() { return &m->TrackManager; }; CLOSTexture& CGameView::GetLOSTexture() { return m->LOSTexture; } CTerritoryTexture& CGameView::GetTerritoryTexture() { return m->TerritoryTexture; } void CGameViewImpl::ScriptingInit() { AddProperty(L"culling", &CGameViewImpl::Culling); AddProperty(L"lockCullCamera", &CGameViewImpl::LockCullCamera); AddProperty(L"constrainCamera", &CGameViewImpl::ConstrainCamera); CJSObject::ScriptingInit("GameView"); } int CGameView::Initialize() { CFG_GET_SYS_VAL("view.scroll.speed", Float, m->ViewScrollSpeed); CFG_GET_SYS_VAL("view.rotate.x.speed", Float, m->ViewRotateXSpeed); CFG_GET_SYS_VAL("view.rotate.x.min", Float, m->ViewRotateXMin); CFG_GET_SYS_VAL("view.rotate.x.max", Float, m->ViewRotateXMax); CFG_GET_SYS_VAL("view.rotate.x.default", Float, m->ViewRotateXDefault); CFG_GET_SYS_VAL("view.rotate.y.speed", Float, m->ViewRotateYSpeed); CFG_GET_SYS_VAL("view.rotate.y.speed.wheel", Float, m->ViewRotateYSpeedWheel); CFG_GET_SYS_VAL("view.rotate.y.default", Float, m->ViewRotateYDefault); CFG_GET_SYS_VAL("view.drag.speed", Float, m->ViewDragSpeed); CFG_GET_SYS_VAL("view.zoom.speed", Float, m->ViewZoomSpeed); CFG_GET_SYS_VAL("view.zoom.speed.wheel", Float, m->ViewZoomSpeedWheel); CFG_GET_SYS_VAL("view.zoom.min", Float, m->ViewZoomMin); CFG_GET_SYS_VAL("view.zoom.max", Float, m->ViewZoomMax); CFG_GET_SYS_VAL("view.zoom.default", Float, m->ViewZoomDefault); CFG_GET_SYS_VAL("joystick.camera.pan.x", Int, m->JoystickPanX); CFG_GET_SYS_VAL("joystick.camera.pan.y", Int, m->JoystickPanY); CFG_GET_SYS_VAL("joystick.camera.rotate.x", Int, m->JoystickRotateX); CFG_GET_SYS_VAL("joystick.camera.rotate.y", Int, m->JoystickRotateY); CFG_GET_SYS_VAL("joystick.camera.zoom.in", Int, m->JoystickZoomIn); CFG_GET_SYS_VAL("joystick.camera.zoom.out", Int, m->JoystickZoomOut); + CFG_GET_SYS_VAL("view.height.smoothness", Float, m->HeightSmoothness); + CFG_GET_SYS_VAL("view.height.min", Float, m->HeightMin); + CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosX.m_Smoothness); CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosY.m_Smoothness); CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosZ.m_Smoothness); CFG_GET_SYS_VAL("view.zoom.smoothness", Float, m->Zoom.m_Smoothness); CFG_GET_SYS_VAL("view.rotate.x.smoothness", Float, m->RotateX.m_Smoothness); CFG_GET_SYS_VAL("view.rotate.y.smoothness", Float, m->RotateY.m_Smoothness); CFG_GET_SYS_VAL("view.near", Float, m->ViewNear); CFG_GET_SYS_VAL("view.far", Float, m->ViewFar); CFG_GET_SYS_VAL("view.fov", Float, m->ViewFOV); // Convert to radians m->RotateX.SetValue(DEGTORAD(m->ViewRotateXDefault)); m->RotateY.SetValue(DEGTORAD(m->ViewRotateYDefault)); m->ViewFOV = DEGTORAD(m->ViewFOV); return 0; } void CGameView::RegisterInit() { // CGameView init RegMemFun(this, &CGameView::Initialize, L"CGameView init", 1); // previously done by CGameView::InitResources RegMemFun(g_TexMan.GetSingletonPtr(), &CTerrainTextureManager::LoadTerrainTextures, L"LoadTerrainTextures", 60); RegMemFun(g_Renderer.GetSingletonPtr(), &CRenderer::LoadAlphaMaps, L"LoadAlphaMaps", 5); RegMemFun(g_Renderer.GetSingletonPtr()->GetWaterManager(), &WaterManager::LoadWaterTextures, L"LoadWaterTextures", 80); } void CGameView::BeginFrame() { if (m->LockCullCamera == false) { // Set up cull camera m->CullCamera = m->ViewCamera; // One way to fix shadows popping in at the edge of the screen is to widen the culling frustum so that // objects aren't culled as early. The downside is that objects will get rendered even though they appear // off screen, which is somewhat inefficient. A better solution would be to decouple shadow map rendering // from model rendering; as it is now, a shadow map is only rendered if its associated model is to be // rendered. // (See http://trac.wildfiregames.com/ticket/504) m->CullCamera.SetProjection(m->ViewNear, m->ViewFar, GetCullFOV()); m->CullCamera.UpdateFrustum(); } g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); CheckLightEnv(); m->Game->CachePlayerColours(); } void CGameView::Render() { g_Renderer.RenderScene(*this); } /////////////////////////////////////////////////////////// // 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 ssize_t patchesPerSide = pTerrain->GetPatchesPerSide(); // find out which patches will be drawn for (ssize_t j=0; jGetPatch(i,j); // can't fail // If the patch is underwater, calculate a bounding box that also contains the water plane CBoundingBoxAligned bounds = patch->GetWorldBounds(); float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight + 0.001f; if(bounds[1].Y < waterHeight) { bounds[1].Y = waterHeight; } - + if (!m->Culling || frustum.IsBoxVisible (CVector3D(0,0,0), bounds)) { //c->Submit(patch); // set the renderstate for this patch patch->setDrawState(true); // set the renderstate for the neighbors CPatch *nPatch; nPatch = pTerrain->GetPatch(i-1,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i-1,j); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i-1,j+1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i,j+1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j+1); if(nPatch) nPatch->setDrawState(true); } } } // draw the patches for (ssize_t j=0; jGetPatch(i,j); // can't fail if(patch->getDrawState() == true) { c->Submit(patch); patch->setDrawState(false); } } } } m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling); } void CGameView::CheckLightEnv() { if (m->CachedLightEnv == g_LightEnv) return; if (m->CachedLightEnv.GetLightingModel() != g_LightEnv.GetLightingModel()) g_Renderer.MakeShadersDirty(); m->CachedLightEnv = g_LightEnv; CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); if (!pTerrain) return; PROFILE("update light env"); pTerrain->MakeDirty(RENDERDATA_UPDATE_COLOR); const std::vector& units = m->Game->GetWorld()->GetUnitManager().GetUnits(); for (size_t i = 0; i < units.size(); ++i) units[i]->GetModel().SetDirtyRec(RENDERDATA_UPDATE_COLOR); } void CGameView::UnloadResources() { g_TexMan.UnloadTerrainTextures(); g_Renderer.UnloadAlphaMaps(); g_Renderer.GetWaterManager()->UnloadWaterTextures(); } - -static void ClampDistance(CGameViewImpl* m, bool smooth) +static void FocusHeight(CGameViewImpl* m, 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->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); - CVector3D forwards = targetCam.m_Orientation.GetIn(); - - CVector3D delta = targetCam.GetFocus() - targetCam.m_Orientation.GetTranslation(); - - float dist = delta.Dot(forwards); - float clampedDist = Clamp(dist, m->ViewZoomMin, m->ViewZoomMax); - float diff = clampedDist - dist; + const CVector3D position = targetCam.m_Orientation.GetTranslation(); + const CVector3D forwards = targetCam.m_Orientation.GetIn(); - if (!diff) + // 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 = m->Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z); + + // filter ground levels for smooth camera movement + const float filtered_near_ground = m->Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius); + const float filtered_pivot_ground = m->Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius); + + // filtered maximum visible ground level in view + const float filtered_ground = std::max(filtered_near_ground, filtered_pivot_ground); + + // 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->PosX.AddSmoothly(forwards.X * -diff); - m->PosY.AddSmoothly(forwards.Y * -diff); - m->PosZ.AddSmoothly(forwards.Z * -diff); + m->PosY.AddSmoothly(diff); } else { - m->PosX.Add(forwards.X * -diff); - m->PosY.Add(forwards.Y * -diff); - m->PosZ.Add(forwards.Z * -diff); + m->PosY.Add(diff); } } +CVector3D CGameView::GetSmoothPivot(CCamera& camera) const +{ + return camera.m_Orientation.GetTranslation() + camera.m_Orientation.GetIn() * m->Zoom.GetSmoothedValue(); +} + void CGameView::Update(float DeltaTime) { // 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; // TODO: this is probably not an ideal place for this, it should probably go // in a CCmpWaterManager or some such thing (once such a thing exists) if (!m->Game->m_Paused) g_Renderer.GetWaterManager()->m_WaterTexTimer += DeltaTime; if (m->TrackManager.IsActive() && m->TrackManager.IsPlaying()) { if (! m->TrackManager.Update(DeltaTime)) { // ResetCamera(); } return; } // 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 * DeltaTime); if (HotkeyIsPressed("camera.rotate.ccw")) m->RotateY.AddSmoothly(-m->ViewRotateYSpeed * DeltaTime); if (HotkeyIsPressed("camera.rotate.up")) m->RotateX.AddSmoothly(-m->ViewRotateXSpeed * DeltaTime); if (HotkeyIsPressed("camera.rotate.down")) m->RotateX.AddSmoothly(m->ViewRotateXSpeed * DeltaTime); 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) { if (g_mouse_x >= g_xres - 2 && g_mouse_x < g_xres) moveRightward += m->ViewScrollSpeed * DeltaTime; else if (g_mouse_x <= 3 && g_mouse_x >= 0) moveRightward -= m->ViewScrollSpeed * DeltaTime; if (g_mouse_y >= g_yres - 2 && g_mouse_y < g_yres) moveForward -= m->ViewScrollSpeed * DeltaTime; else if (g_mouse_y <= 3 && g_mouse_y >= 0) moveForward += m->ViewScrollSpeed * DeltaTime; } if (HotkeyIsPressed("camera.right")) moveRightward += m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.left")) moveRightward -= m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.up")) moveForward += m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.down")) moveForward -= m->ViewScrollSpeed * DeltaTime; if (g_Joystick.IsEnabled()) { // This could all be improved with extra speed and sensitivity settings // (maybe use pow to allow finer control?), and inversion settings moveRightward += g_Joystick.GetAxisValue(m->JoystickPanX) * m->ViewScrollSpeed * DeltaTime; moveForward -= g_Joystick.GetAxisValue(m->JoystickPanY) * m->ViewScrollSpeed * DeltaTime; m->RotateX.AddSmoothly(g_Joystick.GetAxisValue(m->JoystickRotateX) * m->ViewRotateXSpeed * DeltaTime); m->RotateY.AddSmoothly(-g_Joystick.GetAxisValue(m->JoystickRotateY) * m->ViewRotateYSpeed * DeltaTime); // Use a +1 bias for zoom because I want this to work with trigger buttons that default to -1 m->Zoom.AddSmoothly((g_Joystick.GetAxisValue(m->JoystickZoomIn) + 1.0f) / 2.0f * m->ViewZoomSpeed * DeltaTime); m->Zoom.AddSmoothly(-(g_Joystick.GetAxisValue(m->JoystickZoomOut) + 1.0f) / 2.0f * m->ViewZoomSpeed * DeltaTime); } 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(*(m->Game->GetSimulation2()), m->FollowEntity); if (cmpPosition && cmpPosition->IsInWorld()) { // Get the most recent interpolated position float frameOffset = m->Game->GetSimulation2()->GetLastFrameOffset(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset, false); CVector3D pos = transform.GetTranslation(); if (m->FollowFirstPerson) { float x, z, angle; cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle); float height = 4.f; m->ViewCamera.m_Orientation.SetIdentity(); m->ViewCamera.m_Orientation.RotateX((float)M_PI/24.f); m->ViewCamera.m_Orientation.RotateY(angle); m->ViewCamera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z); m->ViewCamera.UpdateFrustum(); return; } else { // Move the camera to match the unit CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); - CVector3D pivot = targetCam.GetFocus(); + 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 * DeltaTime); - if (HotkeyIsPressed("camera.zoom.out")) m->Zoom.AddSmoothly(-m->ViewZoomSpeed * DeltaTime); + if (HotkeyIsPressed("camera.zoom.out")) + m->Zoom.AddSmoothly(m->ViewZoomSpeed * DeltaTime); + + if (m->ConstrainCamera) + m->Zoom.ClampSmoothly(m->ViewZoomMin, m->ViewZoomMax); - float zoomDelta = m->Zoom.Update(DeltaTime); + float zoomDelta = -m->Zoom.Update(DeltaTime); if (zoomDelta) { CVector3D forwards = m->ViewCamera.m_Orientation.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)); - ClampDistance(m, true); + FocusHeight(m, 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->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); - CVector3D pivot = targetCam.GetFocus(); + CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.m_Orientation.GetTranslation() - pivot; CVector3D desiredPivot = pivot; CmpPtr cmpRangeManager(*m->Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpRangeManager && cmpRangeManager->GetLosCircular()) { // Clamp to a circular region around the center of the map float r = pTerrain->GetMaxX() / 2; 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); } // 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(DeltaTime); m->PosY.Update(DeltaTime); m->PosZ.Update(DeltaTime); // Handle rotation around the Y (vertical) axis { CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmooth(m, &targetCam.m_Orientation); float rotateYDelta = m->RotateY.Update(DeltaTime); 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 = targetCam.GetFocus(); + CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.m_Orientation.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->ViewCamera; SetupCameraMatrixSmooth(m, &targetCam.m_Orientation); float rotateXDelta = m->RotateX.Update(DeltaTime); if (rotateXDelta) { CVector3D rightwards = targetCam.m_Orientation.GetLeft() * -1.0f; - CVector3D pivot = m->ViewCamera.GetFocus(); + CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.m_Orientation.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 = m->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(-(float)M_PI, (float)M_PI); // Update the camera matrix m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); } void CGameView::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->ViewCamera; SetupCameraMatrixNonSmooth(m, &targetCam.m_Orientation); - CVector3D pivot = targetCam.GetFocus(); + CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = target - pivot; - + m->PosX.SetValueSmoothly(delta.X + m->PosX.GetValue()); m->PosZ.SetValueSmoothly(delta.Z + m->PosZ.GetValue()); - ClampDistance(m, false); + FocusHeight(m, false); // Break out of following mode so the camera really moves to the target m->FollowEntity = INVALID_ENTITY; } void CGameView::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); - ClampDistance(m, false); + FocusHeight(m, false); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m->FollowEntity = INVALID_ENTITY; } void CGameView::ResetCameraAngleZoom() { CCamera targetCam = m->ViewCamera; SetupCameraMatrixNonSmooth(m, &targetCam.m_Orientation); // Compute the zoom adjustment to get us back to the default CVector3D forwards = targetCam.m_Orientation.GetIn(); - CVector3D delta = targetCam.GetFocus() - targetCam.m_Orientation.GetTranslation(); + + CVector3D pivot = GetSmoothPivot(targetCam); + CVector3D delta = pivot - targetCam.m_Orientation.GetTranslation(); float dist = delta.Dot(forwards); - m->Zoom.AddSmoothly(dist - m->ViewZoomDefault); + m->Zoom.AddSmoothly(m->ViewZoomDefault - dist); // Reset orientations to default m->RotateX.SetValueSmoothly(DEGTORAD(m->ViewRotateXDefault)); m->RotateY.SetValueSmoothly(DEGTORAD(m->ViewRotateYDefault)); } void CGameView::CameraFollow(entity_id_t entity, bool firstPerson) { m->FollowEntity = entity; m->FollowFirstPerson = firstPerson; } entity_id_t CGameView::GetFollowedEntity() { return m->FollowEntity; } float CGameView::GetNear() const { return m->ViewNear; } float CGameView::GetFar() const { return m->ViewFar; } float CGameView::GetFOV() const { return m->ViewFOV; } float CGameView::GetCullFOV() const { return m->ViewFOV + DEGTORAD(6.0f); //add 6 degrees to the default FOV for use with the culling frustum; } void CGameView::SetCameraProjection() { m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); } 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()) 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_HOTKEYDOWN: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "wireframe") { if (g_Renderer.GetModelRenderMode() == SOLID) { g_Renderer.SetTerrainRenderMode(EDGED_FACES); g_Renderer.SetModelRenderMode(EDGED_FACES); } else if (g_Renderer.GetModelRenderMode() == EDGED_FACES) { g_Renderer.SetTerrainRenderMode(WIREFRAME); g_Renderer.SetModelRenderMode(WIREFRAME); } else { g_Renderer.SetTerrainRenderMode(SOLID); g_Renderer.SetModelRenderMode(SOLID); } return IN_HANDLED; } // 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(). else if (hotkey == "camera.zoom.wheel.in") { - m->Zoom.AddSmoothly(m->ViewZoomSpeedWheel); + m->Zoom.AddSmoothly(-m->ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.zoom.wheel.out") { - m->Zoom.AddSmoothly(-m->ViewZoomSpeedWheel); + 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.reset") { ResetCameraAngleZoom(); return IN_HANDLED; } } return IN_PASS; } Index: ps/trunk/source/graphics/Terrain.h =================================================================== --- ps/trunk/source/graphics/Terrain.h (revision 11555) +++ ps/trunk/source/graphics/Terrain.h (revision 11556) @@ -1,172 +1,178 @@ /* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Describes ground via heightmap and array of CPatch. */ #ifndef INCLUDED_TERRAIN #define INCLUDED_TERRAIN #include "maths/Vector3D.h" #include "maths/Fixed.h" #include "graphics/SColor.h" +#include "graphics/HeightMipmap.h" class CPatch; class CMiniPatch; class CFixedVector3D; class CStr8; class CBoundingBoxAligned; /////////////////////////////////////////////////////////////////////////////// // Terrain Constants: /// metres [world space units] per tile in x and z const ssize_t TERRAIN_TILE_SIZE = 4; /// number of u16 height units per metre const ssize_t HEIGHT_UNITS_PER_METRE = 732; // == approx int(256.0f/0.35f) /// metres per u16 height unit const float HEIGHT_SCALE = 1.f / HEIGHT_UNITS_PER_METRE; /////////////////////////////////////////////////////////////////////////////// // CTerrain: main terrain class; contains the heightmap describing elevation // data, and the smaller subpatches that form the terrain class CTerrain { public: CTerrain(); ~CTerrain(); // Coordinate naming convention: world-space coordinates are float x,z; // tile-space coordinates are ssize_t i,j. rationale: signed types can // more efficiently be converted to/from floating point. use ssize_t // instead of int/long because these are sizes. bool Initialize(ssize_t patchesPerSide, const u16* ptr); // return number of vertices along edge of the terrain ssize_t GetVerticesPerSide() const { return m_MapSize; } // return number of tiles along edge of the terrain ssize_t GetTilesPerSide() const { return GetVerticesPerSide()-1; } // return number of patches along edge of the terrain ssize_t GetPatchesPerSide() const { return m_MapSizePatches; } float GetMinX() const { return 0.0f; } float GetMinZ() const { return 0.0f; } float GetMaxX() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); } float GetMaxZ() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); } bool IsOnMap(float x, float z) const { return ((x >= GetMinX()) && (x < GetMaxX()) && (z >= GetMinZ()) && (z < GetMaxZ())); } CStr8 GetMovementClass(ssize_t i, ssize_t j) const; float GetVertexGroundLevel(ssize_t i, ssize_t j) const; fixed GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const; float GetExactGroundLevel(float x, float z) const; fixed GetExactGroundLevelFixed(fixed x, fixed z) const; + float GetFilteredGroundLevel(float x, float z, float radius) const; // get the approximate slope (0 = horizontal, 0.5 = 30 degrees, 1.0 = 45 degrees, etc) fixed GetSlopeFixed(ssize_t i, ssize_t j) const; // Returns true if the triangulation diagonal for tile (i, j) // should be in the direction (1,-1); false if it should be (1,1) bool GetTriangulationDir(ssize_t i, ssize_t j) const; // resize this terrain such that each side has given number of patches void Resize(ssize_t size); // set up a new heightmap from 16 bit data; assumes heightmap matches current terrain size void SetHeightMap(u16* heightmap); // return a pointer to the heightmap u16* GetHeightMap() const { return m_Heightmap; } // get patch at given coordinates, expressed in patch-space; return 0 if // coordinates represent patch off the edge of the map CPatch* GetPatch(ssize_t i, ssize_t j) const; // get tile at given coordinates, expressed in tile-space; return 0 if // coordinates represent tile off the edge of the map CMiniPatch* GetTile(ssize_t i, ssize_t j) const; // calculate the position of a given vertex void CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const; void CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const; // calculate the vertex under a given position (rounding down coordinates) static void CalcFromPosition(const CVector3D& pos, ssize_t& i, ssize_t& j) { i = (ssize_t)(pos.X/TERRAIN_TILE_SIZE); j = (ssize_t)(pos.Z/TERRAIN_TILE_SIZE); } // calculate the vertex under a given position (rounding down coordinates) static void CalcFromPosition(float x, float z, ssize_t& i, ssize_t& j) { i = (ssize_t)(x/TERRAIN_TILE_SIZE); j = (ssize_t)(z/TERRAIN_TILE_SIZE); } // calculate the normal at a given vertex void CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const; void CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const; CVector3D CalcExactNormal(float x, float z) const; // Mark a specific square of tiles (inclusive lower bound, exclusive upper bound) // as dirty - use this after modifying the heightmap. // If you modify a vertex (i,j), you should dirty tiles // from (i-1, j-1) [inclusive] to (i+1, j+1) [exclusive] // since their geometry depends on that vertex. // If you modify a tile (i,j), you should dirty tiles // from (i-1, j-1) [inclusive] to (i+2, j+2) [exclusive] // since their texture blends depend on that tile. void MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags); // mark the entire map as dirty void MakeDirty(int dirtyFlags); /** * Returns a 3D bounding box encompassing the given vertex range (inclusive) */ CBoundingBoxAligned GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1); // get the base colour for the terrain (typically pure white - other colours // will interact badly with LOS - but used by the Actor Viewer tool) SColor4ub GetBaseColour() const { return m_BaseColour; } // set the base colour for the terrain void SetBaseColour(SColor4ub colour) { m_BaseColour = colour; } + const CHeightMipmap& GetHeightMipmap() const { return m_HeightMipmap; } + private: // delete any data allocated by this terrain void ReleaseData(); // setup patch pointers etc void InitialisePatches(); // size of this map in each direction, in vertices; ie. total tiles = sqr(m_MapSize-1) ssize_t m_MapSize; // size of this map in each direction, in patches; total patches = sqr(m_MapSizePatches) ssize_t m_MapSizePatches; // the patches comprising this terrain CPatch* m_Patches; // 16-bit heightmap data u16* m_Heightmap; // base colour (usually white) SColor4ub m_BaseColour; + // heightmap mipmap + CHeightMipmap m_HeightMipmap; }; #endif Index: ps/trunk/source/graphics/HeightMipmap.h =================================================================== --- ps/trunk/source/graphics/HeightMipmap.h (nonexistent) +++ ps/trunk/source/graphics/HeightMipmap.h (revision 11556) @@ -0,0 +1,79 @@ +/* Copyright (C) 2012 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + + +/* + * Describes ground using heightmap mipmaps + * Used for camera movement + */ + +#ifndef INCLUDED_HEIGHTMIPMAP +#define INCLUDED_HEIGHTMIPMAP + +#include "lib/file/vfs/vfs_path.h" + +struct SMipmap +{ + SMipmap() : m_MapSize(0), m_Heightmap(0) { } + SMipmap(size_t MapSize, u16* Heightmap) : m_MapSize(MapSize), m_Heightmap(Heightmap) { } + + size_t m_MapSize; + u16* m_Heightmap; +}; + +class CHeightMipmap +{ + NONCOPYABLE(CHeightMipmap); +public: + + CHeightMipmap(); + ~CHeightMipmap(); + + void Initialize(size_t mapSize, const u16* ptr); + void ReleaseData(); + + // update the heightmap mipmaps + void Update(const u16* ptr); + + // update a section of the heightmap mipmaps + // (coordinates are heightmap cells, inclusive of lower bounds, + // exclusive of upper bounds) + void Update(const u16* ptr, size_t left, size_t bottom, size_t right, size_t top); + + float GetTrilinearGroundLevel(float x, float z, float radius) const; + + void DumpToDisk(const VfsPath& path) const; + +private: + + // get bilinear filtered height from mipmap + float BilinearFilter(const SMipmap &mipmap, float x, float z) const; + + // update rectangle of the output mipmap by bilinear interpolating an input mipmap of exactly twice its size + void HalfResizeUpdate(SMipmap &out_mipmap, size_t mapSize, const u16* ptr, size_t left, size_t bottom, size_t right, size_t top); + + // update rectangle of the output mipmap by bilinear interpolating the input mipmap + void BilinearUpdate(SMipmap &out_mipmap, size_t mapSize, const u16* ptr, size_t left, size_t bottom, size_t right, size_t top); + + // size of this map in each direction + size_t m_MapSize; + + // mipmap list + std::vector m_Mipmap; +}; + +#endif Index: ps/trunk/source/graphics/GameView.h =================================================================== --- ps/trunk/source/graphics/GameView.h (revision 11555) +++ ps/trunk/source/graphics/GameView.h (revision 11556) @@ -1,104 +1,106 @@ /* Copyright (C) 2010 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_GAMEVIEW #define INCLUDED_GAMEVIEW #include "renderer/Scene.h" #include "simulation2/system/Entity.h" #include "lib/input.h" // InReaction - can't forward-declare enum class CGame; class CObjectManager; class CCamera; class CCinemaManager; class CVector3D; struct SViewPort; struct JSObject; class CGameViewImpl; class CGameView : private Scene { NONCOPYABLE(CGameView); private: CGameViewImpl* m; // Check whether lighting environment has changed and update vertex data if necessary void CheckLightEnv(); public: //BEGIN: Implementation of Scene virtual void EnumerateObjects(const CFrustum& frustum, SceneCollector* c); virtual CLOSTexture& GetLOSTexture(); virtual CTerritoryTexture& GetTerritoryTexture(); //END: Implementation of Scene private: // InitResources(): Load all graphics resources (textures, actor objects and // alpha maps) required by the game //void InitResources(); // UnloadResources(): Unload all graphics resources loaded by InitResources void UnloadResources(); public: CGameView(CGame *pGame); ~CGameView(); void SetViewport(const SViewPort& vp); void RegisterInit(); int Initialize(); CObjectManager& GetObjectManager() const; // Update: Update all the view information (i.e. rotate camera, scroll, // whatever). This will *not* change any World information - only the // *presentation* void Update(float DeltaTime); void BeginFrame(); void Render(); InReaction HandleEvent(const SDL_Event_* ev); void MoveCameraTarget(const CVector3D& target); void ResetCameraTarget(const CVector3D& target); void ResetCameraAngleZoom(); void CameraFollow(entity_id_t entity, bool firstPerson); entity_id_t GetFollowedEntity(); + CVector3D GetSmoothPivot(CCamera &camera) const; + float GetNear() const; float GetFar() const; float GetFOV() const; float GetCullFOV() const; // Set projection of current camera using near, far, and FOV values void SetCameraProjection(); CCamera *GetCamera(); CCinemaManager* GetCinema(); JSObject* GetScript(); static void ScriptingInit(); }; extern InReaction game_view_handler(const SDL_Event_* ev); #endif Index: ps/trunk/source/lib/bits.h =================================================================== --- ps/trunk/source/lib/bits.h (revision 11555) +++ ps/trunk/source/lib/bits.h (revision 11556) @@ -1,292 +1,301 @@ /* Copyright (c) 2010 Wildfire Games * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * bit-twiddling. */ #ifndef INCLUDED_BITS #define INCLUDED_BITS /** * value of bit number \. * * @param n bit index. * * requirements: * - T should be an unsigned type * - n must be in [0, CHAR_BIT*sizeof(T)), else the result is undefined! **/ template inline T Bit(size_t n) { const T one = T(1); return (T)(one << n); } /** * pretty much the same as Bit\. * this is intended for the initialization of enum values, where a * compile-time constant is required. **/ #define BIT(n) (1u << (n)) template inline bool IsBitSet(T value, size_t index) { const T bit = Bit(index); return (value & bit) != 0; } // these are declared in the header and inlined to aid compiler optimizations // (they can easily end up being time-critical). // note: GCC can't inline extern functions, while VC's "Whole Program // Optimization" can. /** * a mask that includes the lowest N bits * * @param numBits Number of bits in mask. **/ template inline T bit_mask(size_t numBits) { const T bitsInT = sizeof(T)*CHAR_BIT; const T allBits = (T)~T(0); // (shifts of at least bitsInT are undefined) if(numBits >= bitsInT) return allBits; // (note: the previous allBits >> (bitsInT-numBits) is not safe // because right-shifts of negative numbers are undefined.) const T mask = (T)((T(1) << numBits)-1); return mask; } /** * extract the value of bits hi_idx:lo_idx within num * * example: bits(0x69, 2, 5) == 0x0A * * @param num number whose bits are to be extracted * @param lo_idx bit index of lowest bit to include * @param hi_idx bit index of highest bit to include * @return value of extracted bits. **/ template inline T bits(T num, size_t lo_idx, size_t hi_idx) { const size_t numBits = (hi_idx - lo_idx)+1; // # bits to return T result = T(num >> lo_idx); result = T(result & bit_mask(numBits)); return result; } /** * set the value of bits hi_idx:lo_idx * * @param lo_idx bit index of lowest bit to include * @param hi_idx bit index of highest bit to include * @param value new value to be assigned to these bits **/ template inline T SetBitsTo(T num, size_t lo_idx, size_t hi_idx, size_t value) { const size_t numBits = (hi_idx - lo_idx)+1; ASSERT(value < (T(1) << numBits)); const T mask = bit_mask(numBits) << lo_idx; T result = num & ~mask; result = T(result | (value << lo_idx)); return result; } /** * @return number of 1-bits in mask. * execution time is proportional to number of 1-bits in mask. **/ template inline size_t SparsePopulationCount(T mask) { size_t num1Bits = 0; while(mask) { mask &= mask-1; // clear least significant 1-bit num1Bits++; } return num1Bits; } /** * @return number of 1-bits in mask. * execution time is logarithmic in the total number of bits. * supports up to 128-bit integers (if their arithmetic operators are defined). * [http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel] **/ template static inline size_t PopulationCount(T x) { const T mask = T(~T(0)); x -= (x >> 1) & (mask/3); // count 2 bits x = (x & (mask/15*3)) + ((x >> 2) & (mask/15*3)); // count 4 bits x = (x + (x >> 4)) & (mask/255*15); // count 8 bits return (x * (mask/255)) >> ((sizeof(T)-1)*CHAR_BIT); } /** * @return whether the given number is a power of two. **/ template inline bool is_pow2(T n) { // 0 would pass the test below but isn't a POT. if(n == 0) return false; return (n & (n-1)) == 0; } // as above; intended for use in static_assert #define IS_POW2(n) (((n) != 0) && ((n) & ((n)-1)) == 0) template inline T LeastSignificantBit(T x) { const T negX = T(~x + 1); // 2's complement (avoids 'negating unsigned type' warning) return x & negX; } template inline T ClearLeastSignificantBit(T x) { return x & (x-1); } /** * ceil(log2(x)) * * @param x (unsigned integer) * @return ceiling of the base-2 logarithm (i.e. rounded up) or * zero if the input is zero. **/ template inline size_t ceil_log2(T x) { T bit = 1; size_t log = 0; while(bit < x && bit != 0) // must detect overflow { log++; bit *= 2; } return log; } // compile-time variant of the above template struct CeilLog2 { enum { value = 1 + CeilLog2<(N+1)/2>::value }; }; template<> struct CeilLog2<1> { enum { value = 0 }; }; template<> struct CeilLog2<0> { enum { value = 0 }; }; /** * floor(log2(f)) * fast, uses the FPU normalization hardware. * * @param x (float) input; MUST be > 0, else results are undefined. * @return floor of the base-2 logarithm (i.e. rounded down). **/ extern int floor_log2(const float x); /** * round up to next larger power of two. **/ template inline T round_up_to_pow2(T x) { return T(1) << ceil_log2(x); } /** + * round down to next larger power of two. + **/ +template +inline T round_down_to_pow2(T x) +{ + return T(1) << floor_log2(x); +} + +/** * round number up/down to the next given multiple. * * @param n Number to round. * @param multiple Must be a power of two. **/ template inline T round_up(T n, T multiple) { ASSERT(is_pow2(multiple)); const T result = (n + multiple-1) & ~(multiple-1); ASSERT(n <= result && result < n+multiple); return result; } template inline T round_down(T n, T multiple) { ASSERT(is_pow2(multiple)); const T result = n & ~(multiple-1); ASSERT(result <= n && n < result+multiple); return result; } // evaluates to an expression suitable as an initializer // for constant static data members. #define ROUND_UP(n, multiple) (((n) + (multiple)-1) & ~((multiple)-1)) template inline T MaxPowerOfTwoDivisor(T value) { ASSERT(value != T(0)); for(size_t log2 = 0; log2 < sizeof(T)*CHAR_BIT; log2++) { if(IsBitSet(value, log2)) return T(1) << log2; } DEBUG_WARN_ERR(ERR::LOGIC); // unreachable (!= 0 => there is a set bit) return 0; } #endif // #ifndef INCLUDED_BITS Index: ps/trunk/source/lib/tests/test_bits.h =================================================================== --- ps/trunk/source/lib/tests/test_bits.h (revision 11555) +++ ps/trunk/source/lib/tests/test_bits.h (revision 11556) @@ -1,160 +1,169 @@ /* Copyright (c) 2010 Wildfire Games * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lib/self_test.h" #include "lib/bits.h" -#define EQUALS(actual, expected) ENSURE((actual) == (expected)) +//#define EQUALS(actual, expected) ENSURE((actual) == (expected)) +#define EQUALS TS_ASSERT_EQUALS class TestBits : public CxxTest::TestSuite { public: void test_Bit() { - EQUALS(Bit(0), 1); - EQUALS(Bit(8), 0x100); + EQUALS(Bit(0), 1u); + EQUALS(Bit(8), 0x100u); EQUALS(Bit(31), u32(0x80000000ul)); EQUALS(Bit(1), u64(2)); EQUALS(Bit(32), u64(0x100000000ull)); EQUALS(Bit(63), u64(0x8000000000000000ull)); } void test_IsBitSet() { EQUALS(IsBitSet(0u, 1), false); EQUALS(IsBitSet(1u, 1), false); EQUALS(IsBitSet(2u, 1), true); EQUALS(IsBitSet(0xFFFFFFFFul, 0), true); EQUALS(IsBitSet(0xFFFFFFFFul, 31), true); EQUALS(IsBitSet(0xFFFFFFFFFFFFFFFFull, 0), true); EQUALS(IsBitSet(0xFFFFFFFFFFFFFFFFull, 31), true); EQUALS(IsBitSet(0xFFFFFFFFFFFFFFFFull, 32), true); EQUALS(IsBitSet(0xFFFFFFFFFFFFFFFFull, 63), true); } void test_bit_mask() { EQUALS(bit_mask(0), 0); EQUALS(bit_mask(2), 0x3); EQUALS(bit_mask(16), 0xFFFF); - EQUALS(bit_mask(0), 0); - EQUALS(bit_mask(2), 0x3); + EQUALS(bit_mask(0), 0u); + EQUALS(bit_mask(2), 0x3u); EQUALS(bit_mask(32), 0xFFFFFFFFul); - EQUALS(bit_mask(0), 0); - EQUALS(bit_mask(2), 0x3); + EQUALS(bit_mask(0), 0u); + EQUALS(bit_mask(2), 0x3u); EQUALS(bit_mask(32), 0xFFFFFFFFull); EQUALS(bit_mask(64), 0xFFFFFFFFFFFFFFFFull); } void test_bits() { EQUALS(bits(0xFFFF, 0, 15), 0xFFFF); EQUALS(bits(0xFFFF, 0, 7), 0xFF); EQUALS(bits(0xFFFF, 8, 15), 0xFF); EQUALS(bits(0xFFFF, 14, 15), 0x3); EQUALS(bits(0xAA55, 4, 11), 0xA5); EQUALS(bits(0xAA55, 14, 15), 0x2); EQUALS(bits(0ul, 0, 31), 0ul); EQUALS(bits(0xFFFFFFFFul, 0, 31), 0xFFFFFFFFul); EQUALS(bits(0ull, 0, 63), 0ull); EQUALS(bits(0xFFFFFFFFull, 0, 31), 0xFFFFFFFFull); EQUALS(bits(0x0000FFFFFFFF0000ull, 16, 47), 0xFFFFFFFFull); EQUALS(bits(0xFFFFFFFFFFFFFFFFull, 0, 63), 0xFFFFFFFFFFFFFFFFull); EQUALS(bits(0xA5A5A5A5A5A5A5A5ull, 32, 63), 0xA5A5A5A5ull); } void test_PopulationCount() { - EQUALS(PopulationCount(0), 0); - EQUALS(PopulationCount(4), 1); - EQUALS(PopulationCount(0x28), 2); - EQUALS(PopulationCount(0xFF), 8); - EQUALS(PopulationCount(0x0ul), 0); - EQUALS(PopulationCount(0x8ul), 1); - EQUALS(PopulationCount(0xFFFFul), 16); - EQUALS(PopulationCount(0xFFFFFFFFul), 32); - EQUALS(PopulationCount(0x0ull), 0); - EQUALS(PopulationCount(0x10ull), 1); - EQUALS(PopulationCount(0xFFFFull), 16); - EQUALS(PopulationCount(0xFFFFFFFFull), 32); - EQUALS(PopulationCount(0xFFFFFFFFFFFFFFFEull), 63); - EQUALS(PopulationCount(0xFFFFFFFFFFFFFFFFull), 64); + EQUALS(PopulationCount(0), 0u); + EQUALS(PopulationCount(4), 1u); + EQUALS(PopulationCount(0x28), 2u); + EQUALS(PopulationCount(0xFF), 8u); + EQUALS(PopulationCount(0x0ul), 0u); + EQUALS(PopulationCount(0x8ul), 1u); + EQUALS(PopulationCount(0xFFFFul), 16u); + EQUALS(PopulationCount(0xFFFFFFFFul), 32u); + EQUALS(PopulationCount(0x0ull), 0u); + EQUALS(PopulationCount(0x10ull), 1u); + EQUALS(PopulationCount(0xFFFFull), 16u); + EQUALS(PopulationCount(0xFFFFFFFFull), 32u); + EQUALS(PopulationCount(0xFFFFFFFFFFFFFFFEull), 63u); + EQUALS(PopulationCount(0xFFFFFFFFFFFFFFFFull), 64u); } void test_is_pow2() { EQUALS(is_pow2(0u), false); EQUALS(is_pow2(~0u), false); EQUALS(is_pow2(0x80000001), false); EQUALS(is_pow2(1), true); EQUALS(is_pow2(1u << 31), true); } void test_ceil_log2() { EQUALS(ceil_log2(3u), 2u); EQUALS(ceil_log2(0xffffffffu), 32u); EQUALS(ceil_log2(1u), 0u); EQUALS(ceil_log2(256u), 8u); EQUALS(ceil_log2(0x80000000u), 31u); } void test_floor_log2() { EQUALS(floor_log2(1.f), 0); EQUALS(floor_log2(3.f), 1); EQUALS(floor_log2(256.f), 8); } void test_round_up_to_pow2() { EQUALS(round_up_to_pow2(0u), 1u); EQUALS(round_up_to_pow2(1u), 1u); EQUALS(round_up_to_pow2(127u), 128u); EQUALS(round_up_to_pow2(128u), 128u); EQUALS(round_up_to_pow2(129u), 256u); } + void test_round_down_to_pow2() + { + EQUALS(round_down_to_pow2(1u), 1u); + EQUALS(round_down_to_pow2(127u), 64u); + EQUALS(round_down_to_pow2(128u), 128u); + EQUALS(round_down_to_pow2(129u), 128u); + } + void test_round_up() { EQUALS(round_up( 0u, 16u), 0u); EQUALS(round_up( 4u, 16u), 16u); EQUALS(round_up(15u, 16u), 16u); EQUALS(round_up(20u, 32u), 32u); EQUALS(round_up(29u, 32u), 32u); EQUALS(round_up(0x1000u, 0x1000u), 0x1000u); EQUALS(round_up(0x1001u, 0x1000u), 0x2000u); EQUALS(round_up(0x1900u, 0x1000u), 0x2000u); } void test_round_down() { EQUALS(round_down( 0u, 16u), 0u); EQUALS(round_down( 4u, 16u), 0u); EQUALS(round_down(15u, 16u), 0u); EQUALS(round_down(20u, 16u), 16u); EQUALS(round_down(29u, 16u), 16u); EQUALS(round_down(0x1900u, 0x1000u), 0x1000u); EQUALS(round_down(0x2001u, 0x2000u), 0x2000u); } }; Index: ps/trunk/source/maths/tests/test_Brush.h =================================================================== --- ps/trunk/source/maths/tests/test_Brush.h (revision 11555) +++ ps/trunk/source/maths/tests/test_Brush.h (revision 11556) @@ -1,183 +1,183 @@ /* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "maths/Brush.h" #include "maths/BoundingBoxAligned.h" #include "graphics/Frustum.h" class TestBrush : public CxxTest::TestSuite { public: void setUp() { CxxTest::setAbortTestOnFail(true); } void tearDown() { } void test_slice_empty_brush() { // verifies that the result of slicing an empty bound with a plane yields an empty bound CBrush brush; CPlane plane(CVector4D(0, 0, -1, 0.5f)); // can be anything, really CBrush result; brush.Slice(plane, result); TS_ASSERT(brush.IsEmpty()); } void test_slice_plane_simple() { // slice a 1x1x1 cube vertically down the middle at z = 0.5, with the plane normal pointing towards the negative // end of the Z axis (i.e., anything with Z lower than 0.5 is considered 'in front of' the plane and is kept) CPlane plane(CVector4D(0, 0, -1, 0.5f)); CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); CBrush result; brush.Slice(plane, result); // verify that the resulting brush consists of exactly our 8 expected, unique vertices - TS_ASSERT_EQUALS(8, result.GetVertices().size()); + TS_ASSERT_EQUALS((size_t)8, result.GetVertices().size()); size_t LBF = GetUniqueVertexIndex(result, CVector3D(0, 0, 0)); // left-bottom-front <=> XYZ size_t RBF = GetUniqueVertexIndex(result, CVector3D(1, 0, 0)); // right-bottom-front size_t RBB = GetUniqueVertexIndex(result, CVector3D(1, 0, 0.5f)); // right-bottom-back size_t LBB = GetUniqueVertexIndex(result, CVector3D(0, 0, 0.5f)); // etc. size_t LTF = GetUniqueVertexIndex(result, CVector3D(0, 1, 0)); size_t RTF = GetUniqueVertexIndex(result, CVector3D(1, 1, 0)); size_t RTB = GetUniqueVertexIndex(result, CVector3D(1, 1, 0.5f)); size_t LTB = GetUniqueVertexIndex(result, CVector3D(0, 1, 0.5f)); // verify that the brush contains the six expected planes (one of which is the slicing plane) VerifyFacePresent(result, 5, LBF, RBF, RBB, LBB, LBF); // bottom face VerifyFacePresent(result, 5, LTF, RTF, RTB, LTB, LTF); // top face VerifyFacePresent(result, 5, LBF, LBB, LTB, LTF, LBF); // left face VerifyFacePresent(result, 5, RBF, RBB, RTB, RTF, RBF); // right face VerifyFacePresent(result, 5, LBF, RBF, RTF, LTF, LBF); // front face VerifyFacePresent(result, 5, LBB, RBB, RTB, LTB, LBB); // back face } void test_slice_plane_behind_brush() { // slice the (0,0,0) to (1,1,1) cube by the plane z = 1.5, with the plane normal pointing towards the negative // end of the Z axis (i.e. the entire cube is 'in front of' the plane and should be kept) CPlane plane(CVector4D(0, 0, -1, 1.5f)); CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); CBrush result; brush.Slice(plane, result); // verify that the resulting brush consists of exactly our 8 expected, unique vertices - TS_ASSERT_EQUALS(8, result.GetVertices().size()); + TS_ASSERT_EQUALS((size_t)8, result.GetVertices().size()); size_t LBF = GetUniqueVertexIndex(result, CVector3D(0, 0, 0)); // left-bottom-front <=> XYZ size_t RBF = GetUniqueVertexIndex(result, CVector3D(1, 0, 0)); // right-bottom-front size_t RBB = GetUniqueVertexIndex(result, CVector3D(1, 0, 1)); // right-bottom-back size_t LBB = GetUniqueVertexIndex(result, CVector3D(0, 0, 1)); // etc. size_t LTF = GetUniqueVertexIndex(result, CVector3D(0, 1, 0)); size_t RTF = GetUniqueVertexIndex(result, CVector3D(1, 1, 0)); size_t RTB = GetUniqueVertexIndex(result, CVector3D(1, 1, 1)); size_t LTB = GetUniqueVertexIndex(result, CVector3D(0, 1, 1)); // verify that the brush contains the six expected planes (one of which is the slicing plane) VerifyFacePresent(result, 5, LBF, RBF, RBB, LBB, LBF); // bottom face VerifyFacePresent(result, 5, LTF, RTF, RTB, LTB, LTF); // top face VerifyFacePresent(result, 5, LBF, LBB, LTB, LTF, LBF); // left face VerifyFacePresent(result, 5, RBF, RBB, RTB, RTF, RBF); // right face VerifyFacePresent(result, 5, LBF, RBF, RTF, LTF, LBF); // front face VerifyFacePresent(result, 5, LBB, RBB, RTB, LTB, LBB); // back face } void test_slice_plane_in_front_of_brush() { // slices the (0,0,0) to (1,1,1) cube by the plane z = -0.5, with the plane normal pointing towards the negative // end of the Z axis (i.e. the entire cube is 'behind' the plane and should be cut away) CPlane plane(CVector4D(0, 0, -1, -0.5f)); CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); CBrush result; brush.Slice(plane, result); - TS_ASSERT_EQUALS(0, result.GetVertices().size()); + TS_ASSERT_EQUALS((size_t)0, result.GetVertices().size()); std::vector > faces; result.GetFaces(faces); - TS_ASSERT_EQUALS(0, faces.size()); + TS_ASSERT_EQUALS((size_t)0, faces.size()); } private: size_t GetUniqueVertexIndex(const CBrush& brush, const CVector3D& vertex, float eps = 1e-6f) { std::vector vertices = brush.GetVertices(); for (size_t i = 0; i < vertices.size(); ++i) { const CVector3D& v = vertices[i]; if (fabs(v.X - vertex.X) < eps && fabs(v.Y - vertex.Y) < eps && fabs(v.Z - vertex.Z) < eps) return i; } TS_FAIL("Vertex not found in brush"); return ~0u; } void VerifyFacePresent(const CBrush& brush, int count, ...) { std::vector face; va_list args; va_start(args, count); for (int x = 0; x < count; ++x) face.push_back(va_arg(args, size_t)); va_end(args); if (face.size() == 0) return; std::vector > faces; brush.GetFaces(faces); // the brush is free to use any starting vertex along the face, and to use any winding order, so have 'face' // cycle through various starting values and see if any of them (or their reverse) matches one found in the brush. for (size_t c = 0; c < face.size() - 1; ++c) { std::vector >::iterator it1 = std::find(faces.begin(), faces.end(), face); if (it1 != faces.end()) return; // no match, try the reverse std::vector faceReverse = face; std::reverse(faceReverse.begin(), faceReverse.end()); std::vector >::iterator it2 = std::find(faces.begin(), faces.end(), faceReverse); if (it2 != faces.end()) return; // no match, cycle it face.erase(face.begin()); face.push_back(face[0]); } TS_FAIL("Face not found in brush"); } }; Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 11555) +++ ps/trunk/binaries/data/config/default.cfg (revision 11556) @@ -1,236 +1,238 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * On Linux / OS X, create: * ; * ~/.config/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%/0ad/config/local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; System settings: fancywater = true shadows = true shadowpcf = true vsync = false nos3tc = false noautomipmap = true novbo = false noframebufferobject = false ; Disable hardware cursors nohwcursor = false ; Linux only: Set the driconf force_s3tc_enable option at startup, ; for compressed texture support force_s3tc_enable = true ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ; Prefer GLSL shaders over ARB shaders (not recommended) preferglsl = false ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Opt-in online user reporting system userreport.url = "http://feedback.wildfiregames.com/report/upload/v1/" ; Colour of the sky (in "r g b" format) skycolor = "0 0 0" ; GENERAL PREFERENCES: sound.mastergain = 0.5 ; Camera control settings view.scroll.speed = 120.0 view.rotate.x.speed = 1.2 view.rotate.x.min = 28.0 view.rotate.x.max = 60.0 view.rotate.x.default = 35.0 view.rotate.y.speed = 2.0 view.rotate.y.speed.wheel = 0.45 view.rotate.y.default = 0.0 view.drag.speed = 0.5 view.zoom.speed = 256.0 view.zoom.speed.wheel = 32.0 view.zoom.min = 50.0 view.zoom.max = 200.0 view.zoom.default = 120.0 view.pos.smoothness = 0.1 view.zoom.smoothness = 0.4 view.rotate.x.smoothness = 0.5 view.rotate.y.smoothness = 0.3 view.near = 2.0 ; Near plane distance view.far = 4096.0 ; Far plane distance view.fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide +view.height.smoothness = 0.5 +view.height.min = 16 ; HOTKEY MAPPINGS: ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+' and enclose the entire thing ; in doublequotes. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS hotkey.exit = "Alt+F4", "Ctrl+Break" ; Exit to desktop hotkey.leave = Escape ; End current game or Exit hotkey.pause = Pause ; Pause/unpause game hotkey.screenshot = F2 ; Take PNG screenshot hotkey.bigscreenshot = "Shift+F2" ; Take large BMP screenshot hotkey.togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode hotkey.screenshot.watermark = "K" ; Toggle product/company watermark for official screenshots hotkey.wireframe = "Alt+W" ; Toggle wireframe mode ; > CAMERA SETTINGS hotkey.camera.reset = "H" ; Reset camera rotation to default. hotkey.camera.follow = "F" ; Follow the first unit in the selection hotkey.camera.zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control) hotkey.camera.zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) hotkey.camera.zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) hotkey.camera.zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) hotkey.camera.rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards hotkey.camera.rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards hotkey.camera.rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain hotkey.camera.rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain hotkey.camera.rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) hotkey.camera.rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) hotkey.camera.pan = MouseMiddle, ForwardSlash ; Enable scrolling by moving mouse hotkey.camera.left = A, LeftArrow ; Scroll or rotate left hotkey.camera.right = D, RightArrow ; Scroll or rotate right hotkey.camera.up = W, UpArrow ; Scroll or rotate up/forwards hotkey.camera.down = S, DownArrow ; Scroll or rotate down/backwards ; > CONSOLE SETTINGS hotkey.console.toggle = BackQuote, F9 ; Open/close console ; > CLIPBOARD CONTROLS hotkey.copy = "Ctrl+C" ; Copy to clipboard hotkey.paste = "Ctrl+V" ; Paste from clipboard hotkey.cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > ENTITY SELECTION hotkey.selection.add = Shift ; Add units to selection hotkey.selection.milonly = Alt ; Add only military units to selection hotkey.selection.remove = Ctrl ; Remove units from selection hotkey.selection.idleworker = Period ; Select next idle worker hotkey.selection.idlewarrior = Comma ; Select next idle warrior hotkey.selection.offscreen = Alt ; Include offscreen units in selection hotkey.selection.group.select.0 = 0 hotkey.selection.group.save.0 = "Ctrl+0" hotkey.selection.group.add.0 = "Shift+0" hotkey.selection.group.select.1 = 1 hotkey.selection.group.save.1 = "Ctrl+1" hotkey.selection.group.add.1 = "Shift+1" hotkey.selection.group.select.2 = 2 hotkey.selection.group.save.2 = "Ctrl+2" hotkey.selection.group.add.2 = "Shift+2" hotkey.selection.group.select.3 = 3 hotkey.selection.group.save.3 = "Ctrl+3" hotkey.selection.group.add.3 = "Shift+3" hotkey.selection.group.select.4 = 4 hotkey.selection.group.save.4 = "Ctrl+4" hotkey.selection.group.add.4 = "Shift+4" hotkey.selection.group.select.5 = 5 hotkey.selection.group.save.5 = "Ctrl+5" hotkey.selection.group.add.5 = "Shift+5" hotkey.selection.group.select.6 = 6 hotkey.selection.group.save.6 = "Ctrl+6" hotkey.selection.group.add.6 = "Shift+6" hotkey.selection.group.select.7 = 7 hotkey.selection.group.save.7 = "Ctrl+7" hotkey.selection.group.add.7 = "Shift+7" hotkey.selection.group.select.8 = 8 hotkey.selection.group.save.8 = "Ctrl+8" hotkey.selection.group.add.8 = "Shift+8" hotkey.selection.group.select.9 = 9 hotkey.selection.group.save.9 = "Ctrl+9" hotkey.selection.group.add.9 = "Shift+9" ; > SESSION CONTROLS hotkey.session.kill = Delete ; Destroy selected units hotkey.session.garrison = Ctrl ; Modifier to garrison when clicking on building hotkey.session.queue = Shift ; Modifier to queue unit orders instead of replacing hotkey.session.batchtrain = Shift ; Modifier to train units in batches hotkey.session.massbarter = Shift ; Modifier to barter bunch of resources hotkey.session.unloadtype = Shift ; Modifier to unload all units of type hotkey.session.deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting hotkey.session.rotate.cw = RightBracket ; Rotate building placement preview clockwise hotkey.session.rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise hotkey.timewarp.fastforward = Space ; If timewarp mode enabled, speed up the game hotkey.timewarp.rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game ; > OVERLAY KEYS hotkey.fps.toggle = "Alt+F" ; Toggle frame counter hotkey.session.devcommands.toggle = "Alt+D" ; Toggle developer commands panel hotkey.session.gui.toggle = "Alt+G" ; Toggle visibility of session GUI hotkey.menu.toggle = "F10" ; Toggle in-game menu hotkey.timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ; > HOTKEYS ONLY hotkey.chat = Return ; Toggle chat window ; > GUI TEXTBOX HOTKEYS hotkey.text.delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor hotkey.text.delete.right = "Ctrl+Del" ; Delete word to the right of cursor hotkey.text.move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor hotkey.text.move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor ; > PROFILER hotkey.profile.toggle = "F11" ; Enable/disable real-time profiler hotkey.profile.save = "Shift+F11" ; Save current profiler data to logs/profile.txt hotkey.profile2.enable = "F11" ; Enable HTTP/GPU modes for new profiler profiler2.http.autoenable = false ; Enable HTTP server output at startup (default off for security/performance) profiler2.gpu.autoenable = false ; Enable GPU timing at startup (default off for performance/compatibility) profiler2.gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available profiler2.gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available profiler2.gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available hotkey.attackmove = Super ; > QUICKSAVE hotkey.quicksave = "Shift+F5" hotkey.quickload = "Shift+F8" ; EXPERIMENTAL: joystick/gamepad settings joystick.enable = false joystick.deadzone = 8192 joystick.camera.pan.x = 0 joystick.camera.pan.y = 1 joystick.camera.rotate.x = 3 joystick.camera.rotate.y = 2 joystick.camera.zoom.in = 5 joystick.camera.zoom.out = 4