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"
36 extern int g_xres
, g_yres
;
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
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;
110 CRenderableObject
* renderable
;
125 enum { EDGE_IN
, EDGE_OUT
, POINT
};
127 // Entry is essentially:
129 // u16 id; // index into occluders array
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; }
146 std::vector
<u16
> m_Ids
;
155 ssize_t sz
= m_Ids
.size();
156 for (ssize_t i
= sz
-1; i
>= 0; --i
)
160 m_Ids
[i
] = m_Ids
[sz
-1];
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));
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
);
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
);
264 PROFILE("compute bounds");
266 for (size_t i
= 0; i
< m_SubmittedModelOccluders
.size(); ++i
)
268 CModel
* occluder
= m_SubmittedModelOccluders
[i
];
271 d
.renderable
= occluder
;
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
)
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
];
293 d
.renderable
= occluder
;
296 ComputeScreenBounds(d
, occluder
->GetWorldBounds(), proj
);
298 // Skip zero-sized occluders
299 if (d
.x0
== d
.x1
|| d
.y0
== d
.y1
)
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
;
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);
331 std::sort(entries
.begin(), entries
.end());
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
);
347 else if (type
== EDGE_OUT
)
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
)
359 if (caster
.z
< occluder
.z
)
362 // No point checking further if both are already being rendered
363 if (caster
.rendered
&& occluder
.rendered
)
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
))
378 if (!occluder
.renderable
->GetWorldBounds().RayIntersect(pos
, cameraPos
- pos
, tmin
, tmax
))
383 caster
.rendered
= true;
384 occluder
.rendered
= true;
392 for (size_t i
= 0; i
< occluders
.size(); ++i
)
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
);
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
));
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())
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())
462 // TODO: use CCanvas2D for drawing rects.
465 m
.Scale(1.0f
, -1.f
, 1.0f
);
466 m
.Translate(0.0f
, (float)g_yres
, -1000.0f
);
469 proj
.SetOrtho(0.f
, g_MaxCoord
, 0.f
, g_MaxCoord
, -1.f
, 1000.f
);
474 m_ShaderTech
= g_Renderer
.GetShaderManager().LoadEffect(
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
[] =
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();