Optimise silhouette rendering.
Do some intersection tests on the CPU so that the silhouette render passes only have to draw models/patches that might actually contribute to silhouettes, saving the CPU and GPU cost of rendering more objects than necessary. This was SVN commit r15483.
This commit is contained in:
parent
ffd6e10edf
commit
eb7955599a
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2011 Wildfire Games.
|
||||
/* Copyright (C) 2014 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -22,8 +22,11 @@
|
||||
#include "precompiled.h"
|
||||
|
||||
#include "HFTracer.h"
|
||||
#include "Terrain.h"
|
||||
|
||||
#include "graphics/Patch.h"
|
||||
#include "graphics/Terrain.h"
|
||||
#include "maths/BoundingBoxAligned.h"
|
||||
#include "maths/MathUtil.h"
|
||||
#include "maths/Vector3D.h"
|
||||
|
||||
// To cope well with points that are slightly off the edge of the map,
|
||||
@ -50,8 +53,8 @@ CHFTracer::CHFTracer(CTerrain *pTerrain):
|
||||
// RayTriIntersect: intersect a ray with triangle defined by vertices
|
||||
// v0,v1,v2; return true if ray hits triangle at distance less than dist,
|
||||
// or false otherwise
|
||||
bool CHFTracer::RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2,
|
||||
const CVector3D& origin, const CVector3D& dir, float& dist) const
|
||||
static bool RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2,
|
||||
const CVector3D& origin, const CVector3D& dir, float& dist)
|
||||
{
|
||||
const float EPSILON=0.00001f;
|
||||
|
||||
@ -227,3 +230,129 @@ bool CHFTracer::RayIntersect(const CVector3D& origin, const CVector3D& dir, int&
|
||||
// fell off end of heightmap with no intersection; return a miss
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool TestTile(u16* heightmap, int stride, int i, int j, const CVector3D& pos, const CVector3D& dir, CVector3D& isct)
|
||||
{
|
||||
u16 y00 = heightmap[i + j*stride];
|
||||
u16 y10 = heightmap[i+1 + j*stride];
|
||||
u16 y01 = heightmap[i + (j+1)*stride];
|
||||
u16 y11 = heightmap[i+1 + (j+1)*stride];
|
||||
|
||||
CVector3D p00( i * TERRAIN_TILE_SIZE, y00 * HEIGHT_SCALE, j * TERRAIN_TILE_SIZE);
|
||||
CVector3D p10((i+1) * TERRAIN_TILE_SIZE, y10 * HEIGHT_SCALE, j * TERRAIN_TILE_SIZE);
|
||||
CVector3D p01( i * TERRAIN_TILE_SIZE, y01 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
|
||||
CVector3D p11((i+1) * TERRAIN_TILE_SIZE, y11 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
|
||||
|
||||
int mid1 = y00+y11;
|
||||
int mid2 = y01+y10;
|
||||
int triDir = (mid1 < mid2);
|
||||
|
||||
float dist = FLT_MAX;
|
||||
|
||||
if (triDir)
|
||||
{
|
||||
if (RayTriIntersect(p00, p10, p01, pos, dir, dist) || // lower-left triangle
|
||||
RayTriIntersect(p11, p01, p10, pos, dir, dist)) // upper-right triangle
|
||||
{
|
||||
isct = pos + dir * dist;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (RayTriIntersect(p00, p11, p01, pos, dir, dist) || // upper-left triangle
|
||||
RayTriIntersect(p00, p10, p11, pos, dir, dist)) // lower-right triangle
|
||||
{
|
||||
isct = pos + dir * dist;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CHFTracer::PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out)
|
||||
{
|
||||
// (TODO: This largely duplicates RayIntersect - some refactoring might be
|
||||
// nice in the future.)
|
||||
|
||||
// General approach:
|
||||
// Given the ray defined by origin + dir * t, we increase t until it
|
||||
// enters the patch's bounding box. The x,z coordinates identify which
|
||||
// tile it is currently above/below. Do an intersection test vs the tile's
|
||||
// two triangles. If it doesn't hit, do a 2D line rasterisation to find
|
||||
// the next tiles the ray will pass through, and test each of them.
|
||||
|
||||
// Start by jumping to the point where the ray enters the bounding box
|
||||
CBoundingBoxAligned bound = patch->GetWorldBounds();
|
||||
float tmin, tmax;
|
||||
if (!bound.RayIntersect(origin, dir, tmin, tmax))
|
||||
{
|
||||
// Ray missed patch; no intersection
|
||||
return false;
|
||||
}
|
||||
|
||||
int heightmapStride = patch->m_Parent->GetVerticesPerSide();
|
||||
|
||||
// Get heightmap, offset to start at this patch
|
||||
u16* heightmap = patch->m_Parent->GetHeightMap() +
|
||||
patch->m_X * PATCH_SIZE +
|
||||
patch->m_Z * PATCH_SIZE * heightmapStride;
|
||||
|
||||
// Get patch-space position of ray origin and bbox entry point
|
||||
CVector3D patchPos(
|
||||
patch->m_X * PATCH_SIZE * TERRAIN_TILE_SIZE,
|
||||
0.0f,
|
||||
patch->m_Z * PATCH_SIZE * TERRAIN_TILE_SIZE);
|
||||
CVector3D originPatch = origin - patchPos;
|
||||
CVector3D entryPatch = originPatch + dir * tmin;
|
||||
|
||||
// We want to do a simple 2D line rasterisation (with the 3D ray projected
|
||||
// down onto the Y plane). That will tell us which cells are intersected
|
||||
// in 2D dimensions, then we can do a more precise 3D intersection test.
|
||||
//
|
||||
// WLOG, assume the ray has direction dir.x > 0, dir.z > 0, and starts in
|
||||
// cell (i,j). The next cell intersecting the line must be either (i+1,j)
|
||||
// or (i,j+1). To tell which, just check whether the point (i+1,j+1) is
|
||||
// above or below the ray. Advance into that cell and repeat.
|
||||
//
|
||||
// (If the ray passes precisely through (i+1,j+1), we can pick either.
|
||||
// If the ray is parallel to Y, only the first cell matters, then we can
|
||||
// carry on rasterising in any direction (a bit of a waste of time but
|
||||
// should be extremely rare, and it's safe and simple).)
|
||||
|
||||
// Work out which tile we're starting in
|
||||
int i = clamp((int)(entryPatch.X / TERRAIN_TILE_SIZE), 0, (int)PATCH_SIZE-1);
|
||||
int j = clamp((int)(entryPatch.Z / TERRAIN_TILE_SIZE), 0, (int)PATCH_SIZE-1);
|
||||
|
||||
// Work out which direction the ray is going in
|
||||
int di = (dir.X >= 0 ? 1 : 0);
|
||||
int dj = (dir.Z >= 0 ? 1 : 0);
|
||||
|
||||
do
|
||||
{
|
||||
CVector3D isct;
|
||||
if (TestTile(heightmap, heightmapStride, i, j, originPatch, dir, isct))
|
||||
{
|
||||
if (out)
|
||||
*out = isct + patchPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the vertex between the two possible next cells
|
||||
float nx = (i + di) * (int)TERRAIN_TILE_SIZE;
|
||||
float nz = (j + dj) * (int)TERRAIN_TILE_SIZE;
|
||||
|
||||
// Test which side of the ray the vertex is on, and advance into the
|
||||
// appropriate cell, using a test that works for all 4 combinations
|
||||
// of di,dj
|
||||
float dot = dir.Z * (nx - originPatch.X) - dir.X * (nz - originPatch.Z);
|
||||
if ((di == dj) == (dot > 0.0f))
|
||||
j += dj*2-1;
|
||||
else
|
||||
i += di*2-1;
|
||||
}
|
||||
while (i >= 0 && j >= 0 && i < PATCH_SIZE && j < PATCH_SIZE);
|
||||
|
||||
// Ran off the edge of the patch, so no intersection
|
||||
return false;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2009 Wildfire Games.
|
||||
/* Copyright (C) 2014 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -22,6 +22,7 @@
|
||||
#ifndef INCLUDED_HFTRACER
|
||||
#define INCLUDED_HFTRACER
|
||||
|
||||
class CPatch;
|
||||
class CVector3D;
|
||||
class CTerrain;
|
||||
|
||||
@ -37,13 +38,24 @@ public:
|
||||
// occurs (and fill in grid coordinates and point of intersection), or false otherwise
|
||||
bool RayIntersect(const CVector3D& origin, const CVector3D& dir, int& x, int& z, CVector3D& ipt) const;
|
||||
|
||||
private:
|
||||
// intersect a ray with triangle defined by vertices
|
||||
// v0,v1,v2; return true if ray hits triangle at distance less than dist,
|
||||
// or false otherwise
|
||||
bool RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2,
|
||||
const CVector3D& origin, const CVector3D& dir, float& dist) const;
|
||||
/**
|
||||
* Intersects ray with a single patch.
|
||||
* The ray is a half-infinite line starting at @p origin with direction @p dir
|
||||
* (not required to be a unit vector).. The patch is treated as a collection
|
||||
* of two-sided triangles, corresponding to the terrain tiles.
|
||||
*
|
||||
* If there is an intersection, returns true; and if @p out is not NULL, it
|
||||
* is set to the intersection point. This is guaranteed to be the earliest
|
||||
* tile intersected (starting at @p origin), but not necessarily the earlier
|
||||
* triangle inside that tile.
|
||||
*
|
||||
* This partly duplicates RayIntersect, but it only operates on a single
|
||||
* patch, and it's more precise (it uses the same tile triangulation as the
|
||||
* renderer), and tries to be more numerically robust.
|
||||
*/
|
||||
static bool PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out);
|
||||
|
||||
private:
|
||||
// test if ray intersects either of the triangles in the given
|
||||
bool CellIntersect(int cx, int cz, const CVector3D& origin, const CVector3D& dir, float& dist) const;
|
||||
|
||||
|
@ -18,7 +18,6 @@
|
||||
#ifndef INCLUDED_GRAPHICS_OVERLAY
|
||||
#define INCLUDED_GRAPHICS_OVERLAY
|
||||
|
||||
#include "graphics/RenderableObject.h"
|
||||
#include "graphics/Texture.h"
|
||||
#include "maths/Vector2D.h"
|
||||
#include "maths/Vector3D.h"
|
||||
|
@ -18,6 +18,7 @@
|
||||
#ifndef INCLUDED_SHADERPROGRAM
|
||||
#define INCLUDED_SHADERPROGRAM
|
||||
|
||||
#include "graphics/ShaderProgramPtr.h"
|
||||
#include "graphics/Texture.h"
|
||||
#include "lib/ogl.h"
|
||||
#include "lib/file/vfs/vfs_path.h"
|
||||
@ -204,6 +205,4 @@ protected:
|
||||
int m_ValidStreams; // which streams have been specified via VertexPointer etc since the last Bind
|
||||
};
|
||||
|
||||
typedef shared_ptr<CShaderProgram> CShaderProgramPtr;
|
||||
|
||||
#endif // INCLUDED_SHADERPROGRAM
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
// necessary includes
|
||||
#include "Vector3D.h"
|
||||
#include "graphics/ShaderProgram.h"
|
||||
#include "graphics/ShaderProgramPtr.h"
|
||||
|
||||
class CFrustum;
|
||||
class CMatrix3D;
|
||||
|
@ -45,25 +45,6 @@ int CVector3D::operator ! () const
|
||||
return 1;
|
||||
}
|
||||
|
||||
float CVector3D::Dot (const CVector3D &vector) const
|
||||
{
|
||||
return ( X * vector.X +
|
||||
Y * vector.Y +
|
||||
Z * vector.Z );
|
||||
}
|
||||
|
||||
CVector3D CVector3D::Cross (const CVector3D &vector) const
|
||||
{
|
||||
CVector3D Temp;
|
||||
|
||||
Temp.X = (Y * vector.Z) - (Z * vector.Y);
|
||||
Temp.Y = (Z * vector.X) - (X * vector.Z);
|
||||
Temp.Z = (X * vector.Y) - (Y * vector.X);
|
||||
|
||||
return Temp;
|
||||
}
|
||||
|
||||
|
||||
float CVector3D::LengthSquared () const
|
||||
{
|
||||
return ( SQR(X) + SQR(Y) + SQR(Z) );
|
||||
|
@ -96,8 +96,21 @@ class CVector3D
|
||||
}
|
||||
|
||||
public:
|
||||
float Dot (const CVector3D &vector) const;
|
||||
CVector3D Cross (const CVector3D &vector) const;
|
||||
float Dot (const CVector3D &vector) const
|
||||
{
|
||||
return ( X * vector.X +
|
||||
Y * vector.Y +
|
||||
Z * vector.Z );
|
||||
}
|
||||
|
||||
CVector3D Cross (const CVector3D &vector) const
|
||||
{
|
||||
CVector3D Temp;
|
||||
Temp.X = (Y * vector.Z) - (Z * vector.Y);
|
||||
Temp.Y = (Z * vector.X) - (X * vector.Z);
|
||||
Temp.Z = (X * vector.Y) - (Y * vector.X);
|
||||
return Temp;
|
||||
}
|
||||
|
||||
float Length () const;
|
||||
float LengthSquared () const;
|
||||
|
@ -65,6 +65,7 @@
|
||||
#include "renderer/ParticleRenderer.h"
|
||||
#include "renderer/RenderModifiers.h"
|
||||
#include "renderer/ShadowMap.h"
|
||||
#include "renderer/SilhouetteRenderer.h"
|
||||
#include "renderer/SkyManager.h"
|
||||
#include "renderer/TerrainOverlay.h"
|
||||
#include "renderer/TerrainRenderer.h"
|
||||
@ -293,6 +294,8 @@ public:
|
||||
|
||||
CFontManager fontManager;
|
||||
|
||||
SilhouetteRenderer silhouetteRenderer;
|
||||
|
||||
/// Various model renderers
|
||||
struct Models
|
||||
{
|
||||
@ -1300,7 +1303,6 @@ SScreenRect CRenderer::RenderRefractions(const CShaderDefines& context, const CB
|
||||
return screenScissor;
|
||||
}
|
||||
|
||||
|
||||
void CRenderer::RenderSilhouettes(const CShaderDefines& context)
|
||||
{
|
||||
PROFILE3_GPU("silhouettes");
|
||||
@ -1338,18 +1340,18 @@ void CRenderer::RenderSilhouettes(const CShaderDefines& context)
|
||||
// protrude into the ground, only occlude with the back faces of the
|
||||
// terrain (so silhouettes will still display when behind hills)
|
||||
glCullFace(GL_FRONT);
|
||||
m->terrainRenderer.RenderPatches(CULL_DEFAULT);
|
||||
m->terrainRenderer.RenderPatches(CULL_SILHOUETTE_OCCLUDER);
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE("render model occluders");
|
||||
m->CallModelRenderers(contextOccluder, CULL_DEFAULT, MODELFLAG_SILHOUETTE_OCCLUDER);
|
||||
m->CallModelRenderers(contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0);
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE("render transparent occluders");
|
||||
m->CallTranspModelRenderers(contextOccluder, CULL_DEFAULT, MODELFLAG_SILHOUETTE_OCCLUDER);
|
||||
m->CallTranspModelRenderers(contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0);
|
||||
}
|
||||
|
||||
glDepthFunc(GL_GEQUAL);
|
||||
@ -1377,14 +1379,10 @@ void CRenderer::RenderSilhouettes(const CShaderDefines& context)
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
}
|
||||
|
||||
// TODO: For performance, we probably ought to do a quick raycasting check
|
||||
// to see which units are likely blocked by occluders and not bother
|
||||
// rendering any of the others
|
||||
|
||||
{
|
||||
PROFILE("render models");
|
||||
m->CallModelRenderers(contextDisplay, CULL_DEFAULT, MODELFLAG_SILHOUETTE_DISPLAY);
|
||||
// (This won't render transparent objects with SILHOUETTE_DISPLAY - will
|
||||
PROFILE("render casters");
|
||||
m->CallModelRenderers(contextDisplay, CULL_SILHOUETTE_CASTER, 0);
|
||||
// (This won't render transparent objects with SILHOUETTE_CASTER - will
|
||||
// we have any units that need that?)
|
||||
}
|
||||
|
||||
@ -1606,6 +1604,8 @@ void CRenderer::RenderSubmissions(const CBoundingBoxAligned& waterScissor)
|
||||
ogl_WarnIfError();
|
||||
}
|
||||
|
||||
m->silhouetteRenderer.RenderDebugOverlays(m_ViewCamera);
|
||||
|
||||
// render overlays that should appear on top of all other objects
|
||||
m->overlayRenderer.RenderForegroundOverlays(m_ViewCamera);
|
||||
ogl_WarnIfError();
|
||||
@ -1622,6 +1622,7 @@ void CRenderer::EndFrame()
|
||||
m->terrainRenderer.EndFrame();
|
||||
m->overlayRenderer.EndFrame();
|
||||
m->particleRenderer.EndFrame();
|
||||
m->silhouetteRenderer.EndFrame();
|
||||
|
||||
// Finish model renderers
|
||||
m->Model.NormalSkinned->EndFrame();
|
||||
@ -1712,10 +1713,15 @@ SViewPort CRenderer::GetViewport()
|
||||
void CRenderer::Submit(CPatch* patch)
|
||||
{
|
||||
if (m_CurrentCullGroup == CULL_DEFAULT)
|
||||
{
|
||||
m->shadow.AddShadowReceiverBound(patch->GetWorldBounds());
|
||||
m->silhouetteRenderer.AddOccluder(patch);
|
||||
}
|
||||
|
||||
if (m_CurrentCullGroup == CULL_SHADOWS)
|
||||
{
|
||||
m->shadow.AddShadowCasterBound(patch->GetWorldBounds());
|
||||
}
|
||||
|
||||
m->terrainRenderer.Submit(m_CurrentCullGroup, patch);
|
||||
}
|
||||
@ -1772,6 +1778,11 @@ void CRenderer::SubmitNonRecursive(CModel* model)
|
||||
if (m_CurrentCullGroup == CULL_DEFAULT)
|
||||
{
|
||||
m->shadow.AddShadowReceiverBound(model->GetWorldBounds());
|
||||
|
||||
if (model->GetFlags() & MODELFLAG_SILHOUETTE_OCCLUDER)
|
||||
m->silhouetteRenderer.AddOccluder(model);
|
||||
if (model->GetFlags() & MODELFLAG_SILHOUETTE_DISPLAY)
|
||||
m->silhouetteRenderer.AddCaster(model);
|
||||
}
|
||||
|
||||
if (m_CurrentCullGroup == CULL_SHADOWS)
|
||||
@ -1815,6 +1826,20 @@ void CRenderer::RenderScene(Scene& scene)
|
||||
|
||||
m->particleManager.RenderSubmit(*this, frustum);
|
||||
|
||||
if (m_Options.m_Silhouettes)
|
||||
{
|
||||
m->silhouetteRenderer.ComputeSubmissions(m_ViewCamera);
|
||||
|
||||
m_CurrentCullGroup = CULL_DEFAULT;
|
||||
m->silhouetteRenderer.RenderSubmitOverlays(*this);
|
||||
|
||||
m_CurrentCullGroup = CULL_SILHOUETTE_OCCLUDER;
|
||||
m->silhouetteRenderer.RenderSubmitOccluders(*this);
|
||||
|
||||
m_CurrentCullGroup = CULL_SILHOUETTE_CASTER;
|
||||
m->silhouetteRenderer.RenderSubmitCasters(*this);
|
||||
}
|
||||
|
||||
if (m_Caps.m_Shadows && m_Options.m_Shadows && GetRenderPath() == RP_SHADER)
|
||||
{
|
||||
m_CurrentCullGroup = CULL_SHADOWS;
|
||||
|
@ -102,6 +102,8 @@ public:
|
||||
CULL_SHADOWS,
|
||||
CULL_REFLECTIONS,
|
||||
CULL_REFRACTIONS,
|
||||
CULL_SILHOUETTE_OCCLUDER,
|
||||
CULL_SILHOUETTE_CASTER,
|
||||
CULL_MAX
|
||||
};
|
||||
|
||||
|
495
source/renderer/SilhouetteRenderer.cpp
Normal file
495
source/renderer/SilhouetteRenderer.cpp
Normal file
@ -0,0 +1,495 @@
|
||||
/* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "precompiled.h"
|
||||
|
||||
#include "SilhouetteRenderer.h"
|
||||
|
||||
#include "graphics/Camera.h"
|
||||
#include "graphics/HFTracer.h"
|
||||
#include "graphics/Model.h"
|
||||
#include "graphics/Patch.h"
|
||||
#include "graphics/ShaderManager.h"
|
||||
#include "maths/MathUtil.h"
|
||||
#include "ps/Profile.h"
|
||||
#include "renderer/Renderer.h"
|
||||
#include "renderer/Scene.h"
|
||||
|
||||
// For debugging
|
||||
static const bool g_DisablePreciseIntersections = false;
|
||||
|
||||
SilhouetteRenderer::SilhouetteRenderer()
|
||||
{
|
||||
m_DebugEnabled = false;
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::AddOccluder(CPatch* patch)
|
||||
{
|
||||
m_SubmittedPatchOccluders.push_back(patch);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::AddOccluder(CModel* model)
|
||||
{
|
||||
m_SubmittedModelOccluders.push_back(model);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::AddCaster(CModel* model)
|
||||
{
|
||||
m_SubmittedModelCasters.push_back(model);
|
||||
}
|
||||
|
||||
/*
|
||||
* Silhouettes are the solid-coloured versions of units that are rendered when
|
||||
* standing behind a building or terrain, so the player won't lose them.
|
||||
*
|
||||
* The rendering is done in CRenderer::RenderSilhouettes, by rendering the
|
||||
* units (silhouette casters) and buildings/terrain (silhouette occluders)
|
||||
* in an extra pass using depth and stencil buffers. It's very inefficient to
|
||||
* render those objects when they're not actually going to contribute to a
|
||||
* silhouette.
|
||||
*
|
||||
* This class is responsible for finding the subset of casters/occluders
|
||||
* that might contribute to a silhouette and will need to be rendered.
|
||||
*
|
||||
* The algorithm is largely based on sweep-and-prune for detecting intersection
|
||||
* along a single axis:
|
||||
*
|
||||
* First we compute the 2D screen-space bounding box of every occluder, and
|
||||
* their minimum distance from the camera. We also compute the screen-space
|
||||
* position of each caster (approximating them as points, which is not perfect
|
||||
* but almost always good enough).
|
||||
*
|
||||
* We split each occluder's screen-space bounds into a left ('in') edge and
|
||||
* right ('out') edge. We put those edges plus the caster points into a list,
|
||||
* and sort by x coordinate.
|
||||
*
|
||||
* Then we walk through the list, maintaining an active set of occluders.
|
||||
* An 'in' edge will add an occluder to the set, an 'out' edge will remove it.
|
||||
* When we reach a caster point, the active set contains all the occluders that
|
||||
* intersect it in x. We do a quick test of y and depth coordinates against
|
||||
* each occluder in the set. If they pass that test, we do a more precise ray
|
||||
* vs bounding box test (for model occluders) or ray vs patch (for terrain
|
||||
* occluders) to see if we really need to render that caster and occluder.
|
||||
*
|
||||
* Performance relies on the active set being quite small. Given the game's
|
||||
* typical occluder sizes and camera angles, this works out okay.
|
||||
*
|
||||
* We have to do precise ray/patch intersection tests for terrain, because
|
||||
* if we just used the patch's bounding box, pretty much every unit would
|
||||
* be seen as intersecting the patch it's standing on.
|
||||
*
|
||||
* We store screen-space coordinates as 14-bit integers (0..16383) because
|
||||
* that lets us pack and sort the edge/point list efficiently.
|
||||
*/
|
||||
|
||||
static const int MAX_COORD = 16384;
|
||||
|
||||
struct Occluder
|
||||
{
|
||||
CRenderableObject* renderable;
|
||||
bool isPatch;
|
||||
u16 x0, y0, x1, y1;
|
||||
float z;
|
||||
bool rendered;
|
||||
};
|
||||
|
||||
struct Caster
|
||||
{
|
||||
CModel* model;
|
||||
u16 x, y;
|
||||
float z;
|
||||
bool rendered;
|
||||
};
|
||||
|
||||
enum { EDGE_IN, EDGE_OUT, POINT };
|
||||
|
||||
// Entry is essentially:
|
||||
// struct Entry {
|
||||
// u16 id; // index into occluders array
|
||||
// u16 type : 2;
|
||||
// u16 x : 14;
|
||||
// };
|
||||
// where x is in the most significant bits, so that sorting as a uint32_t
|
||||
// is the same as sorting by x. To avoid worrying about endianness and the
|
||||
// compiler's ability to handle bitfields efficiently, we use uint32_t instead
|
||||
// of the actual struct.
|
||||
|
||||
typedef uint32_t Entry;
|
||||
|
||||
static Entry EntryCreate(int type, u16 id, u16 x) { return (x << 18) | (type << 16) | id; }
|
||||
static int EntryGetId(Entry e) { return e & 0xffff; }
|
||||
static int EntryGetType(Entry e) { return (e >> 16) & 3; }
|
||||
|
||||
struct ActiveList
|
||||
{
|
||||
std::vector<u16> m_Ids;
|
||||
|
||||
void Add(u16 id)
|
||||
{
|
||||
m_Ids.push_back(id);
|
||||
}
|
||||
|
||||
void Remove(u16 id)
|
||||
{
|
||||
ssize_t sz = m_Ids.size();
|
||||
for (ssize_t i = sz-1; i >= 0; --i)
|
||||
{
|
||||
if (m_Ids[i] == id)
|
||||
{
|
||||
m_Ids[i] = m_Ids[sz-1];
|
||||
m_Ids.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug_warn(L"Failed to find id");
|
||||
}
|
||||
};
|
||||
|
||||
static void ComputeScreenBounds(Occluder& occluder, const CBoundingBoxAligned& bounds, CMatrix3D& proj)
|
||||
{
|
||||
int x0 = INT_MAX, y0 = INT_MAX, x1 = INT_MIN, y1 = INT_MIN;
|
||||
float z0 = FLT_MAX;
|
||||
for (size_t ix = 0; ix <= 1; ix++)
|
||||
{
|
||||
for (size_t iy = 0; iy <= 1; iy++)
|
||||
{
|
||||
for (size_t iz = 0; iz <= 1; iz++)
|
||||
{
|
||||
CVector4D vec(bounds[ix].X, bounds[iy].Y, bounds[iz].Z, 1.0f);
|
||||
CVector4D svec = proj.Transform(vec);
|
||||
x0 = std::min(x0, MAX_COORD/2 + (int)(MAX_COORD/2 * svec.X / svec.W));
|
||||
y0 = std::min(y0, MAX_COORD/2 + (int)(MAX_COORD/2 * svec.Y / svec.W));
|
||||
x1 = std::max(x1, MAX_COORD/2 + (int)(MAX_COORD/2 * svec.X / svec.W));
|
||||
y1 = std::max(y1, MAX_COORD/2 + (int)(MAX_COORD/2 * svec.Y / svec.W));
|
||||
z0 = std::min(z0, svec.Z / svec.W);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: there must be a quicker way to do this than to test every vertex,
|
||||
// given the symmetry of the bounding box
|
||||
|
||||
occluder.x0 = clamp(x0, 0, MAX_COORD-1);
|
||||
occluder.y0 = clamp(y0, 0, MAX_COORD-1);
|
||||
occluder.x1 = clamp(x1, 0, MAX_COORD-1);
|
||||
occluder.y1 = clamp(y1, 0, MAX_COORD-1);
|
||||
occluder.z = z0;
|
||||
}
|
||||
|
||||
static void ComputeScreenPos(Caster& caster, const CVector3D& pos, CMatrix3D& proj)
|
||||
{
|
||||
CVector4D vec(pos.X, pos.Y, pos.Z, 1.0f);
|
||||
CVector4D svec = proj.Transform(vec);
|
||||
int x = MAX_COORD/2 + (int)(MAX_COORD/2 * svec.X / svec.W);
|
||||
int y = MAX_COORD/2 + (int)(MAX_COORD/2 * svec.Y / svec.W);
|
||||
float z = svec.Z / svec.W;
|
||||
|
||||
caster.x = clamp(x, 0, MAX_COORD-1);
|
||||
caster.y = clamp(y, 0, MAX_COORD-1);
|
||||
caster.z = z;
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::ComputeSubmissions(const CCamera& camera)
|
||||
{
|
||||
PROFILE3("compute silhouettes");
|
||||
|
||||
m_DebugBounds.clear();
|
||||
m_DebugRects.clear();
|
||||
m_DebugSpheres.clear();
|
||||
|
||||
m_VisiblePatchOccluders.clear();
|
||||
m_VisibleModelOccluders.clear();
|
||||
m_VisibleModelCasters.clear();
|
||||
|
||||
std::vector<Occluder> occluders;
|
||||
std::vector<Caster> casters;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
occluders.reserve(m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size());
|
||||
casters.reserve(m_SubmittedModelCasters.size());
|
||||
entries.reserve((m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size()) * 2 + m_SubmittedModelCasters.size());
|
||||
|
||||
CMatrix3D proj = camera.GetViewProjection();
|
||||
|
||||
// Bump the positions of unit casters upwards a bit, so they're not always
|
||||
// detected as intersecting the terrain they're standing on
|
||||
CVector3D posOffset(0.0f, 0.1f, 0.0f);
|
||||
|
||||
#if 0
|
||||
// For debugging ray-patch intersections - casts a ton of rays and draws
|
||||
// a sphere where they intersect
|
||||
extern int g_xres, g_yres;
|
||||
for (int y = 0; y < g_yres; y += 8)
|
||||
{
|
||||
for (int x = 0; x < g_xres; x += 8)
|
||||
{
|
||||
SOverlaySphere sphere;
|
||||
sphere.m_Color = CColor(1, 0, 0, 1);
|
||||
sphere.m_Radius = 0.25f;
|
||||
sphere.m_Center = camera.GetWorldCoordinates(x, y, false);
|
||||
|
||||
CVector3D origin, dir;
|
||||
camera.BuildCameraRay(x, y, origin, dir);
|
||||
|
||||
for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
|
||||
{
|
||||
CPatch* occluder = m_SubmittedPatchOccluders[i];
|
||||
if (CHFTracer::PatchRayIntersect(occluder, origin, dir, &sphere.m_Center))
|
||||
sphere.m_Color = CColor(0, 0, 1, 1);
|
||||
}
|
||||
m_DebugSpheres.push_back(sphere);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
{
|
||||
PROFILE("compute bounds");
|
||||
|
||||
for (size_t i = 0; i < m_SubmittedModelOccluders.size(); ++i)
|
||||
{
|
||||
CModel* occluder = m_SubmittedModelOccluders[i];
|
||||
|
||||
Occluder d;
|
||||
d.renderable = occluder;
|
||||
d.isPatch = false;
|
||||
d.rendered = false;
|
||||
ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
|
||||
|
||||
// Skip zero-sized occluders, so we don't need to worry about EDGE_OUT
|
||||
// getting sorted before EDGE_IN
|
||||
if (d.x0 == d.x1 || d.y0 == d.y1)
|
||||
continue;
|
||||
|
||||
size_t id = occluders.size();
|
||||
occluders.push_back(d);
|
||||
|
||||
entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
|
||||
entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
|
||||
{
|
||||
CPatch* occluder = m_SubmittedPatchOccluders[i];
|
||||
|
||||
Occluder d;
|
||||
d.renderable = occluder;
|
||||
d.isPatch = true;
|
||||
d.rendered = false;
|
||||
ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
|
||||
|
||||
// Skip zero-sized occluders
|
||||
if (d.x0 == d.x1 || d.y0 == d.y1)
|
||||
continue;
|
||||
|
||||
size_t id = occluders.size();
|
||||
occluders.push_back(d);
|
||||
|
||||
entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
|
||||
entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_SubmittedModelCasters.size(); ++i)
|
||||
{
|
||||
CModel* model = m_SubmittedModelCasters[i];
|
||||
CVector3D pos = model->GetTransform().GetTranslation() + posOffset;
|
||||
|
||||
Caster d;
|
||||
d.model = model;
|
||||
d.rendered = false;
|
||||
ComputeScreenPos(d, pos, proj);
|
||||
|
||||
size_t id = casters.size();
|
||||
casters.push_back(d);
|
||||
|
||||
entries.push_back(EntryCreate(POINT, id, d.x));
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the u16 id didn't overflow
|
||||
ENSURE(occluders.size() < 65536 && casters.size() < 65536);
|
||||
|
||||
{
|
||||
PROFILE("sorting");
|
||||
std::sort(entries.begin(), entries.end());
|
||||
}
|
||||
|
||||
{
|
||||
PROFILE("sweeping");
|
||||
|
||||
ActiveList active;
|
||||
CVector3D cameraPos = camera.GetOrientation().GetTranslation();
|
||||
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
Entry e = entries[i];
|
||||
int type = EntryGetType(e);
|
||||
u16 id = EntryGetId(e);
|
||||
if (type == EDGE_IN)
|
||||
active.Add(id);
|
||||
else if (type == EDGE_OUT)
|
||||
active.Remove(id);
|
||||
else
|
||||
{
|
||||
Caster& caster = casters[id];
|
||||
for (size_t j = 0; j < active.m_Ids.size(); ++j)
|
||||
{
|
||||
Occluder& occluder = occluders[active.m_Ids[j]];
|
||||
|
||||
if (caster.y < occluder.y0 || caster.y > occluder.y1)
|
||||
continue;
|
||||
|
||||
if (caster.z < occluder.z)
|
||||
continue;
|
||||
|
||||
// No point checking further if both are already being rendered
|
||||
if (caster.rendered && occluder.rendered)
|
||||
continue;
|
||||
|
||||
if (!g_DisablePreciseIntersections)
|
||||
{
|
||||
CVector3D pos = caster.model->GetTransform().GetTranslation() + posOffset;
|
||||
if (occluder.isPatch)
|
||||
{
|
||||
CPatch* patch = static_cast<CPatch*>(occluder.renderable);
|
||||
if (!CHFTracer::PatchRayIntersect(patch, pos, cameraPos - pos, NULL))
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
float tmin, tmax;
|
||||
if (!occluder.renderable->GetWorldBounds().RayIntersect(pos, cameraPos - pos, tmin, tmax))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
caster.rendered = true;
|
||||
occluder.rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_DebugEnabled)
|
||||
{
|
||||
for (size_t i = 0; i < occluders.size(); ++i)
|
||||
{
|
||||
DebugRect r;
|
||||
r.color = occluders[i].rendered ? CColor(1.0f, 1.0f, 0.0f, 1.0f) : CColor(0.2f, 0.2f, 0.0f, 1.0f);
|
||||
r.x0 = occluders[i].x0;
|
||||
r.y0 = occluders[i].y0;
|
||||
r.x1 = occluders[i].x1;
|
||||
r.y1 = occluders[i].y1;
|
||||
m_DebugRects.push_back(r);
|
||||
|
||||
DebugBounds b;
|
||||
b.color = r.color;
|
||||
b.bounds = occluders[i].renderable->GetWorldBounds();
|
||||
m_DebugBounds.push_back(b);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < occluders.size(); ++i)
|
||||
{
|
||||
if (occluders[i].rendered)
|
||||
{
|
||||
if (occluders[i].isPatch)
|
||||
m_VisiblePatchOccluders.push_back(static_cast<CPatch*>(occluders[i].renderable));
|
||||
else
|
||||
m_VisibleModelOccluders.push_back(static_cast<CModel*>(occluders[i].renderable));
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < casters.size(); ++i)
|
||||
if (casters[i].rendered)
|
||||
m_VisibleModelCasters.push_back(casters[i].model);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::RenderSubmitOverlays(SceneCollector& collector)
|
||||
{
|
||||
for (size_t i = 0; i < m_DebugSpheres.size(); i++)
|
||||
collector.Submit(&m_DebugSpheres[i]);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::RenderSubmitOccluders(SceneCollector& collector)
|
||||
{
|
||||
for (size_t i = 0; i < m_VisiblePatchOccluders.size(); ++i)
|
||||
collector.Submit(m_VisiblePatchOccluders[i]);
|
||||
|
||||
for (size_t i = 0; i < m_VisibleModelOccluders.size(); ++i)
|
||||
collector.SubmitNonRecursive(m_VisibleModelOccluders[i]);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::RenderSubmitCasters(SceneCollector& collector)
|
||||
{
|
||||
for (size_t i = 0; i < m_VisibleModelCasters.size(); ++i)
|
||||
collector.SubmitNonRecursive(m_VisibleModelCasters[i]);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::RenderDebugOverlays(const CCamera& camera)
|
||||
{
|
||||
CShaderTechniquePtr shaderTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid);
|
||||
shaderTech->BeginPass();
|
||||
CShaderProgramPtr shader = shaderTech->GetShader();
|
||||
|
||||
glDepthMask(0);
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
shader->Uniform(str_transform, camera.GetViewProjection());
|
||||
|
||||
for (size_t i = 0; i < m_DebugBounds.size(); ++i)
|
||||
{
|
||||
shader->Uniform(str_color, m_DebugBounds[i].color);
|
||||
m_DebugBounds[i].bounds.RenderOutline(shader);
|
||||
}
|
||||
|
||||
CMatrix3D m;
|
||||
m.SetIdentity();
|
||||
m.Scale(1.0f, -1.f, 1.0f);
|
||||
m.Translate(0.0f, (float)g_yres, -1000.0f);
|
||||
|
||||
CMatrix3D proj;
|
||||
proj.SetOrtho(0.f, MAX_COORD, 0.f, MAX_COORD, -1.f, 1000.f);
|
||||
m = proj * m;
|
||||
|
||||
shader->Uniform(str_transform, proj);
|
||||
|
||||
for (size_t i = 0; i < m_DebugRects.size(); ++i)
|
||||
{
|
||||
const DebugRect& r = m_DebugRects[i];
|
||||
shader->Uniform(str_color, r.color);
|
||||
u16 verts[] = {
|
||||
r.x0, r.y0,
|
||||
r.x1, r.y0,
|
||||
r.x1, r.y1,
|
||||
r.x0, r.y1,
|
||||
r.x0, r.y0,
|
||||
};
|
||||
shader->VertexPointer(2, GL_SHORT, 0, verts);
|
||||
glDrawArrays(GL_LINE_STRIP, 0, 5);
|
||||
}
|
||||
|
||||
shaderTech->EndPass();
|
||||
|
||||
glEnable(GL_CULL_FACE);
|
||||
glDepthMask(1);
|
||||
}
|
||||
|
||||
void SilhouetteRenderer::EndFrame()
|
||||
{
|
||||
m_SubmittedPatchOccluders.clear();
|
||||
m_SubmittedModelOccluders.clear();
|
||||
m_SubmittedModelCasters.clear();
|
||||
}
|
77
source/renderer/SilhouetteRenderer.h
Normal file
77
source/renderer/SilhouetteRenderer.h
Normal file
@ -0,0 +1,77 @@
|
||||
/* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INCLUDED_SILHOUETTERENDERER
|
||||
#define INCLUDED_SILHOUETTERENDERER
|
||||
|
||||
#include "ps/Overlay.h"
|
||||
#include "graphics/Overlay.h"
|
||||
#include "maths/BoundingBoxAligned.h"
|
||||
|
||||
class CCamera;
|
||||
class CModel;
|
||||
class CPatch;
|
||||
class SceneCollector;
|
||||
|
||||
class SilhouetteRenderer
|
||||
{
|
||||
public:
|
||||
SilhouetteRenderer();
|
||||
|
||||
void AddOccluder(CPatch* patch);
|
||||
void AddOccluder(CModel* model);
|
||||
void AddCaster(CModel* model);
|
||||
|
||||
void ComputeSubmissions(const CCamera& camera);
|
||||
void RenderSubmitOverlays(SceneCollector& collector);
|
||||
void RenderSubmitOccluders(SceneCollector& collector);
|
||||
void RenderSubmitCasters(SceneCollector& collector);
|
||||
|
||||
void RenderDebugOverlays(const CCamera& camera);
|
||||
|
||||
void EndFrame();
|
||||
|
||||
private:
|
||||
bool m_DebugEnabled;
|
||||
|
||||
std::vector<CPatch*> m_SubmittedPatchOccluders;
|
||||
std::vector<CModel*> m_SubmittedModelOccluders;
|
||||
std::vector<CModel*> m_SubmittedModelCasters;
|
||||
|
||||
std::vector<CPatch*> m_VisiblePatchOccluders;
|
||||
std::vector<CModel*> m_VisibleModelOccluders;
|
||||
std::vector<CModel*> m_VisibleModelCasters;
|
||||
|
||||
struct DebugBounds
|
||||
{
|
||||
CColor color;
|
||||
CBoundingBoxAligned bounds;
|
||||
};
|
||||
|
||||
struct DebugRect
|
||||
{
|
||||
CColor color;
|
||||
u16 x0, y0, x1, y1;
|
||||
};
|
||||
|
||||
std::vector<DebugBounds> m_DebugBounds;
|
||||
std::vector<DebugRect> m_DebugRects;
|
||||
|
||||
std::vector<SOverlaySphere> m_DebugSpheres;
|
||||
};
|
||||
|
||||
#endif // INCLUDED_SILHOUETTERENDERER
|
Loading…
Reference in New Issue
Block a user