From 769350a3e7845f27456d7ea03195f0be1f9d6ac9 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Wed, 18 Apr 2012 20:39:00 +0000 Subject: [PATCH] Compute camera height and zoom limits based on smoothed terrain heightmap. Fixes #794, based on patch by Dietger. This was SVN commit r11556. --- binaries/data/config/default.cfg | 2 + source/graphics/GameView.cpp | 104 ++++++--- source/graphics/GameView.h | 2 + source/graphics/HeightMipmap.cpp | 261 +++++++++++++++++++++++ source/graphics/HeightMipmap.h | 79 +++++++ source/graphics/Terrain.cpp | 49 ++++- source/graphics/Terrain.h | 6 + source/gui/scripting/ScriptFunctions.cpp | 10 + source/lib/bits.h | 9 + source/lib/tests/test_bits.h | 51 +++-- source/maths/tests/test_Brush.h | 8 +- 11 files changed, 516 insertions(+), 65 deletions(-) create mode 100644 source/graphics/HeightMipmap.cpp create mode 100644 source/graphics/HeightMipmap.h diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 77f9b17992..2f6ebb1eb3 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -96,6 +96,8 @@ 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: diff --git a/source/graphics/GameView.cpp b/source/graphics/GameView.cpp index b5ac97e62c..86e7c26235 100644 --- a/source/graphics/GameView.cpp +++ b/source/graphics/GameView.cpp @@ -190,6 +190,8 @@ public: JoystickRotateY(-1), JoystickZoomIn(-1), JoystickZoomOut(-1), + HeightSmoothness(0.5f), + HeightMin(16.f), PosX(0, 0, 0.01f), PosY(0, 0, 0.01f), @@ -289,6 +291,8 @@ public: int JoystickRotateY; int JoystickZoomIn; int JoystickZoomOut; + float HeightSmoothness; + float HeightMin; //////////////////////////////////////// // Camera Controls State @@ -426,6 +430,9 @@ int CGameView::Initialize() 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); @@ -509,7 +516,7 @@ void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c) if(bounds[1].Y < waterHeight) { bounds[1].Y = waterHeight; } - + if (!m->Culling || frustum.IsBoxVisible (CVector3D(0,0,0), bounds)) { //c->Submit(patch); @@ -595,40 +602,69 @@ void CGameView::UnloadResources() 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(); + const CVector3D position = targetCam.m_Orientation.GetTranslation(); + const CVector3D forwards = targetCam.m_Orientation.GetIn(); - CVector3D delta = targetCam.GetFocus() - targetCam.m_Orientation.GetTranslation(); + // 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; - float dist = delta.Dot(forwards); - float clampedDist = Clamp(dist, m->ViewZoomMin, m->ViewZoomMax); - float diff = clampedDist - dist; + const CVector3D nearPoint = position + forwards * m->ViewNear; + const CVector3D pivotPoint = position + forwards * m->Zoom.GetSmoothedValue(); - if (!diff) + 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, @@ -759,7 +795,7 @@ void CGameView::Update(float DeltaTime) 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); @@ -774,11 +810,14 @@ void CGameView::Update(float DeltaTime) } 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); - float zoomDelta = m->Zoom.Update(DeltaTime); + if (m->ConstrainCamera) + m->Zoom.ClampSmoothly(m->ViewZoomMin, m->ViewZoomMax); + + float zoomDelta = -m->Zoom.Update(DeltaTime); if (zoomDelta) { CVector3D forwards = m->ViewCamera.m_Orientation.GetIn(); @@ -790,7 +829,7 @@ void CGameView::Update(float DeltaTime) 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 @@ -801,7 +840,7 @@ void CGameView::Update(float DeltaTime) CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); - CVector3D pivot = targetCam.GetFocus(); + CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.m_Orientation.GetTranslation() - pivot; CVector3D desiredPivot = pivot; @@ -846,7 +885,7 @@ void CGameView::Update(float DeltaTime) 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; @@ -869,7 +908,7 @@ void CGameView::Update(float DeltaTime) { 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; @@ -919,13 +958,13 @@ void CGameView::MoveCameraTarget(const CVector3D& 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; @@ -944,8 +983,9 @@ void CGameView::ResetCameraTarget(const CVector3D& target) 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(); @@ -961,9 +1001,11 @@ void CGameView::ResetCameraAngleZoom() // 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)); @@ -1049,12 +1091,12 @@ InReaction CGameView::HandleEvent(const SDL_Event_* ev) // 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") diff --git a/source/graphics/GameView.h b/source/graphics/GameView.h index f7bb96c377..feb33d0e49 100644 --- a/source/graphics/GameView.h +++ b/source/graphics/GameView.h @@ -85,6 +85,8 @@ public: 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; diff --git a/source/graphics/HeightMipmap.cpp b/source/graphics/HeightMipmap.cpp new file mode 100644 index 0000000000..286af1b996 --- /dev/null +++ b/source/graphics/HeightMipmap.cpp @@ -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); +} diff --git a/source/graphics/HeightMipmap.h b/source/graphics/HeightMipmap.h new file mode 100644 index 0000000000..6bdaa47f03 --- /dev/null +++ b/source/graphics/HeightMipmap.h @@ -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 diff --git a/source/graphics/Terrain.cpp b/source/graphics/Terrain.cpp index b1e680c336..d2530b9f8d 100644 --- a/source/graphics/Terrain.cpp +++ b/source/graphics/Terrain.cpp @@ -57,6 +57,8 @@ CTerrain::~CTerrain() // ReleaseData: delete any data allocated by this terrain void CTerrain::ReleaseData() { + m_HeightMipmap.ReleaseData(); + delete[] m_Heightmap; delete[] m_Patches; } @@ -65,30 +67,36 @@ void CTerrain::ReleaseData() /////////////////////////////////////////////////////////////////////////////// // 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; } @@ -330,6 +338,17 @@ fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const 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) @@ -530,6 +549,9 @@ void CTerrain::Resize(ssize_t size) // initialise all the new patches InitialisePatches(); + + // initialise mipmap + m_HeightMipmap.Initialize(m_MapSize,m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// @@ -564,6 +586,9 @@ void CTerrain::SetHeightMap(u16* heightmap) patch->SetDirty(RENDERDATA_UPDATE_VERTICES); } } + + // update mipmap + m_HeightMipmap.Update(m_Heightmap); } @@ -587,6 +612,9 @@ void CTerrain::MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dir patch->SetDirty(dirtyFlags); } } + + if (m_Heightmap) + m_HeightMipmap.Update(m_Heightmap, i0, j0, i1, j1); } void CTerrain::MakeDirty(int dirtyFlags) @@ -601,6 +629,9 @@ void CTerrain::MakeDirty(int dirtyFlags) 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) diff --git a/source/graphics/Terrain.h b/source/graphics/Terrain.h index a0f73ceada..7019c94a16 100644 --- a/source/graphics/Terrain.h +++ b/source/graphics/Terrain.h @@ -25,6 +25,7 @@ #include "maths/Vector3D.h" #include "maths/Fixed.h" #include "graphics/SColor.h" +#include "graphics/HeightMipmap.h" class CPatch; class CMiniPatch; @@ -84,6 +85,7 @@ public: 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; @@ -151,6 +153,8 @@ public: // 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(); @@ -167,6 +171,8 @@ private: u16* m_Heightmap; // base colour (usually white) SColor4ub m_BaseColour; + // heightmap mipmap + CHeightMipmap m_HeightMipmap; }; #endif diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp index 4fad858096..f0be317d0b 100644 --- a/source/gui/scripting/ScriptFunctions.cpp +++ b/source/gui/scripting/ScriptFunctions.cpp @@ -531,6 +531,15 @@ void DumpSimState(void* UNUSED(cbdata)) 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); @@ -628,6 +637,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface) scriptInterface.RegisterFunction("DebugWarn"); scriptInterface.RegisterFunction("ForceGC"); scriptInterface.RegisterFunction("DumpSimState"); + scriptInterface.RegisterFunction("DumpTerrainMipmap"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); diff --git a/source/lib/bits.h b/source/lib/bits.h index 5ef37b3519..141449b9fa 100644 --- a/source/lib/bits.h +++ b/source/lib/bits.h @@ -245,6 +245,15 @@ 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. * diff --git a/source/lib/tests/test_bits.h b/source/lib/tests/test_bits.h index ff0ddc8356..c26b396437 100644 --- a/source/lib/tests/test_bits.h +++ b/source/lib/tests/test_bits.h @@ -24,15 +24,16 @@ #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)); @@ -57,11 +58,11 @@ public: 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); } @@ -85,20 +86,20 @@ public: 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() @@ -135,6 +136,14 @@ public: 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); diff --git a/source/maths/tests/test_Brush.h b/source/maths/tests/test_Brush.h index 4153fe5e4e..bb16fd587a 100644 --- a/source/maths/tests/test_Brush.h +++ b/source/maths/tests/test_Brush.h @@ -56,7 +56,7 @@ public: 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 @@ -86,7 +86,7 @@ public: 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 @@ -115,12 +115,12 @@ public: 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: