Skip to content

Commit

Permalink
CityOnPlanet: optimize instanced rendering by up to 25%
Browse files Browse the repository at this point in the history
- Precalculate instance transforms for each potentially-visible building at startup rather than every single frame.
- Move all matrix multiplication out of the inner loop and do frustum culling with an optimized single-precision float frustum structure.
- Pass a CameraContext rather than a Frustum to CityOnPlanet::Render for top-level frustum culling.
- Add comments indicating potential future performance improvements.
  • Loading branch information
sturnclaw committed Oct 31, 2024
1 parent c6faa67 commit 18fd318
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 38 deletions.
98 changes: 65 additions & 33 deletions src/CityOnPlanet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "CityOnPlanet.h"

#include "Camera.h"
#include "FileSystem.h"
#include "Frame.h"
#include "Game.h"
Expand All @@ -11,19 +12,22 @@
#include "Pi.h"
#include "Planet.h"
#include "SpaceStation.h"

#include "collider/Geom.h"
#include "core/Log.h"

#include "graphics/Frustum.h"
#include "graphics/Graphics.h"
#include "graphics/Material.h"
#include "graphics/RenderState.h"
#include "graphics/Renderer.h"
#include "graphics/Types.h"

#include "scenegraph/Animation.h"
#include "scenegraph/Model.h"
#include "scenegraph/ModelSkin.h"
#include "scenegraph/SceneGraph.h"
#include "utils.h"

#include "matrix3x3.h"
#include "matrix4x4.h"

#include "utils.h"

Expand Down Expand Up @@ -87,6 +91,26 @@ void CityOnPlanet::RemoveStaticGeomsFromCollisionSpace()
}
}

void CityOnPlanet::PrecalcInstanceTransforms(const matrix4x4d &stationOrient)
{
PROFILE_SCOPED();

m_instanceTransforms.clear();

// Precalculate building orientation matrices
matrix3x3f rot[4] = { matrix3x3f(stationOrient.GetOrient()) };
for (int i = 1; i < 4; i++) {
rot[i] = rot[0] * matrix3x3f::RotateY(M_PI * 0.5 * double(i));
}

// Cache building instance transforms
// TODO: this could easily be represented as a 4x3 matrix or dual quaternion for significantly less bandwidth
// That requires an "instance buffer" that isn't a 4x4 matrix per instance though... :(
for (const auto &building : m_enabledBuildings) {
m_instanceTransforms.emplace_back(rot[building.rotation], building.pos);
}
}

void CityOnPlanet::GetModelSize(const Aabb &aabb, uint8_t size[2])
{
vector3d aabbSize = aabb.max - aabb.min;
Expand Down Expand Up @@ -326,6 +350,8 @@ CityOnPlanet::CityOnPlanet(Planet *planet, SpaceStation *station, const Uint32 s
Generate(station);

AddStaticGeomsToCollisionSpace();

PrecalcInstanceTransforms(station->GetOrient());
}

void CityOnPlanet::Generate(SpaceStation *station)
Expand Down Expand Up @@ -374,7 +400,8 @@ void CityOnPlanet::Generate(SpaceStation *station)

// top-left corner of the grid is -X, -Z relative to station position at center
// offset origin by half-grid to ensure grid centers are aligned with the station model
m_gridOrigin = station->GetPosition() - incX * (cityExtents + 0.5) - incZ * (cityExtents + 0.5);
const vector3d stationOrigin = station->GetPosition();
m_gridOrigin = stationOrigin - incX * (cityExtents + 0.5) - incZ * (cityExtents + 0.5);

// Setup the station somewhere in the city (defaults to center for now)
const uint32_t stationPos[2] = {
Expand Down Expand Up @@ -536,7 +563,7 @@ void CityOnPlanet::Generate(SpaceStation *station)
Geom *geom = new Geom(cmesh->GetGeomTree(), orientcalc[orient], pos, GetPlanet());

// add it to the list of buildings to render
m_buildings.push_back({ typeIndex, float(cmesh->GetRadius()), orient, pos, geom });
m_buildings.push_back({ vector3f(pos - stationOrigin), float(cmesh->GetRadius()), typeIndex, orient, geom });

}
}
Expand All @@ -550,10 +577,9 @@ void CityOnPlanet::Generate(SpaceStation *station)

// Compute the total AABB of this city
Aabb totalAABB;
const vector3d &stationOrigin = station->GetPosition();

for (const auto &buildingInst : m_buildings) {
totalAABB.Update(buildingInst.pos - stationOrigin);
totalAABB.Update(vector3d(buildingInst.pos));
}

m_realCentre = totalAABB.min + ((totalAABB.max - totalAABB.min) * 0.5);
Expand Down Expand Up @@ -640,7 +666,7 @@ bool CityOnPlanet::TestGridOccupancy(uint32_t x, uint32_t y, const uint8_t size[
return test != 0;
}

void CityOnPlanet::Render(Graphics::Renderer *r, const Graphics::Frustum &frustum, const SpaceStation *station, const vector3d &viewCoords, const matrix4x4d &viewTransform)
void CityOnPlanet::Render(Graphics::Renderer *r, const CameraContext *camera, const SpaceStation *station, const vector3d &viewCoords, const matrix4x4d &viewTransform)
{
PROFILE_SCOPED()

Expand All @@ -649,31 +675,25 @@ void CityOnPlanet::Render(Graphics::Renderer *r, const Graphics::Frustum &frustu
return;

// Early frustum test of whole city.
const vector3d stationOrigin = station->GetPosition();
const vector3d stationPos = viewTransform * (station->GetPosition() + m_realCentre);
//modelview seems to be always identity
if (!frustum.TestPoint(stationPos, m_clipRadius))

if (!camera->GetFrustum().TestPoint(stationPos, m_clipRadius))
return;

// Precalculate building orientation matrices
matrix4x4d rot[4];
matrix4x4f rotf[4];
rot[0] = station->GetOrient();
// Use the station-centered frame-oriented coordinate system we generated building positions in
matrix4x4f modelView = matrix4x4f(viewTransform * matrix4x4d::Translation(station->GetInterpPosition()));

// Combine all transformations to do as few computations as possible inside of the frustum-test loop
// with as much precision as possible
Graphics::FrustumF frustum = Graphics::FrustumF(modelView, camera->GetProjectionMatrix());

// change detail level if necessary
const bool bDetailChanged = m_detailLevel != Pi::detail.cities;
if (bDetailChanged) {
RemoveStaticGeomsFromCollisionSpace();
AddStaticGeomsToCollisionSpace();
}

rot[0] = viewTransform * rot[0];
for (int i = 1; i < 4; i++) {
rot[i] = rot[0] * matrix4x4d::RotateYMatrix(M_PI * 0.5 * double(i));
}
for (int i = 0; i < 4; i++) {
for (int e = 0; e < 16; e++) {
rotf[i][e] = float(rot[i][e]);
}
PrecalcInstanceTransforms(station->GetOrient());
}

// update any idle animations
Expand All @@ -696,27 +716,39 @@ void CityOnPlanet::Render(Graphics::Renderer *r, const Graphics::Frustum &frustu

transform.resize(numBuildings);

// TODO: we could store all instance transforms in a single array by
// sorting the list of buildings according to instance index...
// Of course, we can't do that because Model::RenderInstanced wants a full-
// owning std::vector of instance data
for (uint32_t i = 0; i < numBuildings; i++) {
transform[i].reserve(m_buildingCounts[i]);
}

for (const auto &building : m_enabledBuildings) {
const vector3d pos = viewTransform * building.pos;
{
// Because we cull each individual instance for performance reasons, we
// have to repack instance transform data into an instance buffer to be
// uploaded to the GPU
// TODO: this could potentially be improved by clustering buildings
// according to a spatial distance metric and then frustum culling those
// clusters instead. It would result in more instanced draws however.

if (!frustum.TestPoint(pos, building.clipRadius))
continue;
PROFILE_SCOPED_DESC("CityOnPlanet::Render() Frustum Culling")

matrix4x4f instanceRot = matrix4x4f(rotf[building.rotation]);
instanceRot.SetTranslate(vector3f(pos));
for (size_t i = 0; i < m_enabledBuildings.size(); i++) {
const auto &building = m_enabledBuildings[i];

transform[building.instIndex].push_back(instanceRot);
++uCount;
if (!frustum.TestPoint(building.pos, building.clipRadius))
continue;

transform[building.instIndex].emplace_back(m_instanceTransforms[i]);
++uCount;
}
}

// render the building models using instancing
for (Uint32 i = 0; i < numBuildings; i++) {
if (!transform[i].empty())
m_cityType->buildingTypes[i].model->RenderInstanced(transform[i]);
m_cityType->buildingTypes[i].model->RenderInstanced(modelView, transform[i]);
}

// Draw debug extents
Expand Down
16 changes: 12 additions & 4 deletions src/CityOnPlanet.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
#include "Random.h"
#include "JsonFwd.h"
#include "matrix4x4.h"
#include "graphics/Frustum.h"

class CameraContext;
class Geom;
class Planet;
class SpaceStation;
Expand Down Expand Up @@ -39,7 +39,7 @@ class CityOnPlanet {
CityOnPlanet(Planet *planet, SpaceStation *station, const Uint32 seed);
virtual ~CityOnPlanet();

void Render(Graphics::Renderer *r, const Graphics::Frustum &camera, const SpaceStation *station, const vector3d &viewCoords, const matrix4x4d &viewTransform);
void Render(Graphics::Renderer *r, const CameraContext *camera, const SpaceStation *station, const vector3d &viewCoords, const matrix4x4d &viewTransform);
inline Planet *GetPlanet() const { return m_planet; }
float GetClipRadius() const { return m_clipRadius; }

Expand Down Expand Up @@ -114,11 +114,15 @@ class CityOnPlanet {
void AddStaticGeomsToCollisionSpace();
void RemoveStaticGeomsFromCollisionSpace();

void PrecalcInstanceTransforms(const matrix4x4d &stationOrient);

// NOTE: position here is stored as a 32-bit float offset from the station
// This should retain decent precision within the size of our cities (5-10km radius).
struct BuildingInstance {
Uint32 instIndex;
vector3f pos;
float clipRadius;
Uint32 instIndex;
int rotation; // 0-3
vector3d pos;
Geom *geom;
};

Expand All @@ -132,10 +136,14 @@ class CityOnPlanet {
FrameId m_frame;
Random m_rand;

// Both of these vectors are sorted by building instance index
std::vector<BuildingInstance> m_buildings;
std::vector<BuildingInstance> m_enabledBuildings;

std::vector<Uint32> m_buildingCounts;

std::vector<matrix4x4f> m_instanceTransforms;

// bitmask occupancy grid for quick population of the city
std::unique_ptr<uint8_t[]> m_gridBitset;
// width of a single grid row in bytes
Expand Down
2 changes: 1 addition & 1 deletion src/SpaceStation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@ void SpaceStation::Render(Graphics::Renderer *r, const Camera *camera, const vec
SetClipRadius(m_adjacentCity->GetClipRadius());
}

m_adjacentCity->Render(r, camera->GetContext()->GetFrustum(), this, viewCoords, viewTransform);
m_adjacentCity->Render(r, camera->GetContext(), this, viewCoords, viewTransform);

RenderModel(r, camera, viewCoords, viewTransform);
m_navLights->Render(r);
Expand Down

0 comments on commit 18fd318

Please sign in to comment.