Merge 'remotes/trunk'
[0ad.git] / source / renderer / SilhouetteRenderer.cpp
blobd7d757a07e5280842d86a2a4019a5e356862cf97
1 /* Copyright (C) 2023 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "SilhouetteRenderer.h"
22 #include "graphics/Camera.h"
23 #include "graphics/HFTracer.h"
24 #include "graphics/Model.h"
25 #include "graphics/Patch.h"
26 #include "graphics/ShaderManager.h"
27 #include "maths/MathUtil.h"
28 #include "ps/CStrInternStatic.h"
29 #include "ps/Profile.h"
30 #include "renderer/DebugRenderer.h"
31 #include "renderer/Renderer.h"
32 #include "renderer/Scene.h"
34 #include <cfloat>
36 extern int g_xres, g_yres;
38 // For debugging
39 static const bool g_DisablePreciseIntersections = false;
41 SilhouetteRenderer::SilhouetteRenderer()
43 m_DebugEnabled = false;
46 void SilhouetteRenderer::AddOccluder(CPatch* patch)
48 m_SubmittedPatchOccluders.push_back(patch);
51 void SilhouetteRenderer::AddOccluder(CModel* model)
53 m_SubmittedModelOccluders.push_back(model);
56 void SilhouetteRenderer::AddCaster(CModel* model)
58 m_SubmittedModelCasters.push_back(model);
62 * Silhouettes are the solid-colored versions of units that are rendered when
63 * standing behind a building or terrain, so the player won't lose them.
65 * The rendering is done in CRenderer::RenderSilhouettes, by rendering the
66 * units (silhouette casters) and buildings/terrain (silhouette occluders)
67 * in an extra pass using depth and stencil buffers. It's very inefficient to
68 * render those objects when they're not actually going to contribute to a
69 * silhouette.
71 * This class is responsible for finding the subset of casters/occluders
72 * that might contribute to a silhouette and will need to be rendered.
74 * The algorithm is largely based on sweep-and-prune for detecting intersection
75 * along a single axis:
77 * First we compute the 2D screen-space bounding box of every occluder, and
78 * their minimum distance from the camera. We also compute the screen-space
79 * position of each caster (approximating them as points, which is not perfect
80 * but almost always good enough).
82 * We split each occluder's screen-space bounds into a left ('in') edge and
83 * right ('out') edge. We put those edges plus the caster points into a list,
84 * and sort by x coordinate.
86 * Then we walk through the list, maintaining an active set of occluders.
87 * An 'in' edge will add an occluder to the set, an 'out' edge will remove it.
88 * When we reach a caster point, the active set contains all the occluders that
89 * intersect it in x. We do a quick test of y and depth coordinates against
90 * each occluder in the set. If they pass that test, we do a more precise ray
91 * vs bounding box test (for model occluders) or ray vs patch (for terrain
92 * occluders) to see if we really need to render that caster and occluder.
94 * Performance relies on the active set being quite small. Given the game's
95 * typical occluder sizes and camera angles, this works out okay.
97 * We have to do precise ray/patch intersection tests for terrain, because
98 * if we just used the patch's bounding box, pretty much every unit would
99 * be seen as intersecting the patch it's standing on.
101 * We store screen-space coordinates as 14-bit integers (0..16383) because
102 * that lets us pack and sort the edge/point list efficiently.
105 static const u16 g_MaxCoord = 1 << 14;
106 static const u16 g_HalfMaxCoord = g_MaxCoord / 2;
108 struct Occluder
110 CRenderableObject* renderable;
111 bool isPatch;
112 u16 x0, y0, x1, y1;
113 float z;
114 bool rendered;
117 struct Caster
119 CModel* model;
120 u16 x, y;
121 float z;
122 bool rendered;
125 enum { EDGE_IN, EDGE_OUT, POINT };
127 // Entry is essentially:
128 // struct Entry {
129 // u16 id; // index into occluders array
130 // u16 type : 2;
131 // u16 x : 14;
132 // };
133 // where x is in the most significant bits, so that sorting as a uint32_t
134 // is the same as sorting by x. To avoid worrying about endianness and the
135 // compiler's ability to handle bitfields efficiently, we use uint32_t instead
136 // of the actual struct.
138 typedef uint32_t Entry;
140 static Entry EntryCreate(int type, u16 id, u16 x) { return (x << 18) | (type << 16) | id; }
141 static int EntryGetId(Entry e) { return e & 0xffff; }
142 static int EntryGetType(Entry e) { return (e >> 16) & 3; }
144 struct ActiveList
146 std::vector<u16> m_Ids;
148 void Add(u16 id)
150 m_Ids.push_back(id);
153 void Remove(u16 id)
155 ssize_t sz = m_Ids.size();
156 for (ssize_t i = sz-1; i >= 0; --i)
158 if (m_Ids[i] == id)
160 m_Ids[i] = m_Ids[sz-1];
161 m_Ids.pop_back();
162 return;
165 debug_warn(L"Failed to find id");
169 static void ComputeScreenBounds(Occluder& occluder, const CBoundingBoxAligned& bounds, CMatrix3D& proj)
171 u16 x0 = std::numeric_limits<u16>::max();
172 u16 y0 = std::numeric_limits<u16>::max();
173 u16 x1 = std::numeric_limits<u16>::min();
174 u16 y1 = std::numeric_limits<u16>::min();
175 float z0 = std::numeric_limits<float>::max();
176 for (size_t ix = 0; ix <= 1; ++ix)
178 for (size_t iy = 0; iy <= 1; ++iy)
180 for (size_t iz = 0; iz <= 1; ++iz)
182 CVector4D svec = proj.Transform(CVector4D(bounds[ix].X, bounds[iy].Y, bounds[iz].Z, 1.0f));
183 x0 = std::min(x0, static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.X / svec.W)));
184 y0 = std::min(y0, static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.Y / svec.W)));
185 x1 = std::max(x1, static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.X / svec.W)));
186 y1 = std::max(y1, static_cast<u16>(g_HalfMaxCoord + static_cast<u16>(g_HalfMaxCoord * svec.Y / svec.W)));
187 z0 = std::min(z0, svec.Z / svec.W);
191 // TODO: there must be a quicker way to do this than to test every vertex,
192 // given the symmetry of the bounding box
194 occluder.x0 = Clamp(x0, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
195 occluder.y0 = Clamp(y0, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
196 occluder.x1 = Clamp(x1, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
197 occluder.y1 = Clamp(y1, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
198 occluder.z = z0;
201 static void ComputeScreenPos(Caster& caster, const CVector3D& pos, CMatrix3D& proj)
203 CVector4D svec = proj.Transform(CVector4D(pos.X, pos.Y, pos.Z, 1.0f));
204 u16 x = g_HalfMaxCoord + static_cast<int>(g_HalfMaxCoord * svec.X / svec.W);
205 u16 y = g_HalfMaxCoord + static_cast<int>(g_HalfMaxCoord * svec.Y / svec.W);
206 caster.x = Clamp(x, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
207 caster.y = Clamp(y, std::numeric_limits<u16>::min(), static_cast<u16>(g_MaxCoord - 1));
208 caster.z = svec.Z / svec.W;
211 void SilhouetteRenderer::ComputeSubmissions(const CCamera& camera)
213 PROFILE3("compute silhouettes");
215 m_DebugBounds.clear();
216 m_DebugRects.clear();
217 m_DebugSpheres.clear();
219 m_VisiblePatchOccluders.clear();
220 m_VisibleModelOccluders.clear();
221 m_VisibleModelCasters.clear();
223 std::vector<Occluder> occluders;
224 std::vector<Caster> casters;
225 std::vector<Entry> entries;
227 occluders.reserve(m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size());
228 casters.reserve(m_SubmittedModelCasters.size());
229 entries.reserve((m_SubmittedModelOccluders.size() + m_SubmittedPatchOccluders.size()) * 2 + m_SubmittedModelCasters.size());
231 CMatrix3D proj = camera.GetViewProjection();
233 // Bump the positions of unit casters upwards a bit, so they're not always
234 // detected as intersecting the terrain they're standing on
235 CVector3D posOffset(0.0f, 0.1f, 0.0f);
237 #if 0
238 // For debugging ray-patch intersections - casts a ton of rays and draws
239 // a sphere where they intersect
240 for (int y = 0; y < g_yres; y += 8)
242 for (int x = 0; x < g_xres; x += 8)
244 SOverlaySphere sphere;
245 sphere.m_Color = CColor(1, 0, 0, 1);
246 sphere.m_Radius = 0.25f;
247 sphere.m_Center = camera.GetWorldCoordinates(x, y, false);
249 CVector3D origin, dir;
250 camera.BuildCameraRay(x, y, origin, dir);
252 for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
254 CPatch* occluder = m_SubmittedPatchOccluders[i];
255 if (CHFTracer::PatchRayIntersect(occluder, origin, dir, &sphere.m_Center))
256 sphere.m_Color = CColor(0, 0, 1, 1);
258 m_DebugSpheres.push_back(sphere);
261 #endif
264 PROFILE("compute bounds");
266 for (size_t i = 0; i < m_SubmittedModelOccluders.size(); ++i)
268 CModel* occluder = m_SubmittedModelOccluders[i];
270 Occluder d;
271 d.renderable = occluder;
272 d.isPatch = false;
273 d.rendered = false;
274 ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
276 // Skip zero-sized occluders, so we don't need to worry about EDGE_OUT
277 // getting sorted before EDGE_IN
278 if (d.x0 == d.x1 || d.y0 == d.y1)
279 continue;
281 u16 id = static_cast<u16>(occluders.size());
282 occluders.push_back(d);
284 entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
285 entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
288 for (size_t i = 0; i < m_SubmittedPatchOccluders.size(); ++i)
290 CPatch* occluder = m_SubmittedPatchOccluders[i];
292 Occluder d;
293 d.renderable = occluder;
294 d.isPatch = true;
295 d.rendered = false;
296 ComputeScreenBounds(d, occluder->GetWorldBounds(), proj);
298 // Skip zero-sized occluders
299 if (d.x0 == d.x1 || d.y0 == d.y1)
300 continue;
302 u16 id = static_cast<u16>(occluders.size());
303 occluders.push_back(d);
305 entries.push_back(EntryCreate(EDGE_IN, id, d.x0));
306 entries.push_back(EntryCreate(EDGE_OUT, id, d.x1));
309 for (size_t i = 0; i < m_SubmittedModelCasters.size(); ++i)
311 CModel* model = m_SubmittedModelCasters[i];
312 CVector3D pos = model->GetTransform().GetTranslation() + posOffset;
314 Caster d;
315 d.model = model;
316 d.rendered = false;
317 ComputeScreenPos(d, pos, proj);
319 u16 id = static_cast<u16>(casters.size());
320 casters.push_back(d);
322 entries.push_back(EntryCreate(POINT, id, d.x));
326 // Make sure the u16 id didn't overflow
327 ENSURE(occluders.size() < 65536 && casters.size() < 65536);
330 PROFILE("sorting");
331 std::sort(entries.begin(), entries.end());
335 PROFILE("sweeping");
337 ActiveList active;
338 CVector3D cameraPos = camera.GetOrientation().GetTranslation();
340 for (size_t i = 0; i < entries.size(); ++i)
342 Entry e = entries[i];
343 int type = EntryGetType(e);
344 u16 id = EntryGetId(e);
345 if (type == EDGE_IN)
346 active.Add(id);
347 else if (type == EDGE_OUT)
348 active.Remove(id);
349 else
351 Caster& caster = casters[id];
352 for (size_t j = 0; j < active.m_Ids.size(); ++j)
354 Occluder& occluder = occluders[active.m_Ids[j]];
356 if (caster.y < occluder.y0 || caster.y > occluder.y1)
357 continue;
359 if (caster.z < occluder.z)
360 continue;
362 // No point checking further if both are already being rendered
363 if (caster.rendered && occluder.rendered)
364 continue;
366 if (!g_DisablePreciseIntersections)
368 CVector3D pos = caster.model->GetTransform().GetTranslation() + posOffset;
369 if (occluder.isPatch)
371 CPatch* patch = static_cast<CPatch*>(occluder.renderable);
372 if (!CHFTracer::PatchRayIntersect(patch, pos, cameraPos - pos, NULL))
373 continue;
375 else
377 float tmin, tmax;
378 if (!occluder.renderable->GetWorldBounds().RayIntersect(pos, cameraPos - pos, tmin, tmax))
379 continue;
383 caster.rendered = true;
384 occluder.rendered = true;
390 if (m_DebugEnabled)
392 for (size_t i = 0; i < occluders.size(); ++i)
394 DebugRect r;
395 r.color = occluders[i].rendered ? CColor(1.0f, 1.0f, 0.0f, 1.0f) : CColor(0.2f, 0.2f, 0.0f, 1.0f);
396 r.x0 = occluders[i].x0;
397 r.y0 = occluders[i].y0;
398 r.x1 = occluders[i].x1;
399 r.y1 = occluders[i].y1;
400 m_DebugRects.push_back(r);
402 DebugBounds b;
403 b.color = r.color;
404 b.bounds = occluders[i].renderable->GetWorldBounds();
405 m_DebugBounds.push_back(b);
409 for (size_t i = 0; i < occluders.size(); ++i)
411 if (occluders[i].rendered)
413 if (occluders[i].isPatch)
414 m_VisiblePatchOccluders.push_back(static_cast<CPatch*>(occluders[i].renderable));
415 else
416 m_VisibleModelOccluders.push_back(static_cast<CModel*>(occluders[i].renderable));
420 for (size_t i = 0; i < casters.size(); ++i)
421 if (casters[i].rendered)
422 m_VisibleModelCasters.push_back(casters[i].model);
425 void SilhouetteRenderer::RenderSubmitOverlays(SceneCollector& collector)
427 for (size_t i = 0; i < m_DebugSpheres.size(); i++)
428 collector.Submit(&m_DebugSpheres[i]);
431 void SilhouetteRenderer::RenderSubmitOccluders(SceneCollector& collector)
433 for (size_t i = 0; i < m_VisiblePatchOccluders.size(); ++i)
434 collector.Submit(m_VisiblePatchOccluders[i]);
436 for (size_t i = 0; i < m_VisibleModelOccluders.size(); ++i)
437 collector.SubmitNonRecursive(m_VisibleModelOccluders[i]);
440 void SilhouetteRenderer::RenderSubmitCasters(SceneCollector& collector)
442 for (size_t i = 0; i < m_VisibleModelCasters.size(); ++i)
443 collector.SubmitNonRecursive(m_VisibleModelCasters[i]);
446 void SilhouetteRenderer::RenderDebugBounds(
447 Renderer::Backend::IDeviceCommandContext* UNUSED(deviceCommandContext))
449 if (m_DebugBounds.empty())
450 return;
452 for (size_t i = 0; i < m_DebugBounds.size(); ++i)
453 g_Renderer.GetDebugRenderer().DrawBoundingBox(m_DebugBounds[i].bounds, m_DebugBounds[i].color, true);
456 void SilhouetteRenderer::RenderDebugOverlays(
457 Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
459 if (m_DebugRects.empty())
460 return;
462 // TODO: use CCanvas2D for drawing rects.
463 CMatrix3D m;
464 m.SetIdentity();
465 m.Scale(1.0f, -1.f, 1.0f);
466 m.Translate(0.0f, (float)g_yres, -1000.0f);
468 CMatrix3D proj;
469 proj.SetOrtho(0.f, g_MaxCoord, 0.f, g_MaxCoord, -1.f, 1000.f);
470 m = proj * m;
472 if (!m_ShaderTech)
474 m_ShaderTech = g_Renderer.GetShaderManager().LoadEffect(
475 str_solid, {},
476 [](Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc)
478 pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE;
479 pipelineStateDesc.rasterizationState.cullMode = Renderer::Backend::CullMode::NONE;
482 const std::array<Renderer::Backend::SVertexAttributeFormat, 1> attributes{{
483 {Renderer::Backend::VertexAttributeStream::POSITION,
484 Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2,
485 Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}
487 m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes);
490 deviceCommandContext->BeginPass();
491 deviceCommandContext->SetGraphicsPipelineState(
492 m_ShaderTech->GetGraphicsPipelineState());
494 Renderer::Backend::IShaderProgram* shader = m_ShaderTech->GetShader();
495 deviceCommandContext->SetUniform(
496 shader->GetBindingSlot(str_transform), proj.AsFloatArray());
498 const int32_t colorBindingSlot = shader->GetBindingSlot(str_color);
499 for (const DebugRect& r : m_DebugRects)
501 deviceCommandContext->SetUniform(
502 colorBindingSlot, r.color.AsFloatArray());
503 const float vertices[] =
505 r.x0, r.y0,
506 r.x1, r.y0,
507 r.x1, r.y1,
508 r.x0, r.y0,
509 r.x1, r.y1,
510 r.x0, r.y1,
513 deviceCommandContext->SetVertexInputLayout(m_VertexInputLayout);
515 deviceCommandContext->SetVertexBufferData(
516 0, vertices, std::size(vertices) * sizeof(vertices[0]));
518 deviceCommandContext->Draw(0, 6);
521 deviceCommandContext->EndPass();
524 void SilhouetteRenderer::EndFrame()
526 m_SubmittedPatchOccluders.clear();
527 m_SubmittedModelOccluders.clear();
528 m_SubmittedModelCasters.clear();