Line data Source code
1 : /* Copyright (C) 2023 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
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.
8 : *
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.
13 : *
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/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "TexturedLineRData.h"
21 :
22 : #include "graphics/ShaderProgram.h"
23 : #include "graphics/Terrain.h"
24 : #include "maths/Frustum.h"
25 : #include "maths/MathUtil.h"
26 : #include "maths/Quaternion.h"
27 : #include "ps/CStrInternStatic.h"
28 : #include "renderer/OverlayRenderer.h"
29 : #include "renderer/Renderer.h"
30 : #include "simulation2/Simulation2.h"
31 : #include "simulation2/system/SimContext.h"
32 : #include "simulation2/components/ICmpWaterManager.h"
33 :
34 : /* Note: this implementation uses g_VBMan directly rather than access it through the nicer VertexArray interface,
35 : * because it allows you to work with variable amounts of vertices and indices more easily. New code should prefer
36 : * to use VertexArray where possible, though. */
37 :
38 : // static
39 6 : Renderer::Backend::IVertexInputLayout* CTexturedLineRData::GetVertexInputLayout()
40 : {
41 6 : const uint32_t stride = sizeof(CTexturedLineRData::SVertex);
42 6 : const std::array<Renderer::Backend::SVertexAttributeFormat, 3> attributes{{
43 : {Renderer::Backend::VertexAttributeStream::POSITION,
44 : Renderer::Backend::Format::R32G32B32_SFLOAT,
45 : offsetof(CTexturedLineRData::SVertex, m_Position), stride,
46 : Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
47 : {Renderer::Backend::VertexAttributeStream::UV0,
48 : Renderer::Backend::Format::R32G32_SFLOAT,
49 : offsetof(CTexturedLineRData::SVertex, m_UV), stride,
50 : Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
51 : {Renderer::Backend::VertexAttributeStream::UV1,
52 : Renderer::Backend::Format::R32G32_SFLOAT,
53 : offsetof(CTexturedLineRData::SVertex, m_UV), stride,
54 : Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}
55 : }};
56 6 : return g_Renderer.GetVertexInputLayout(attributes);
57 : }
58 :
59 0 : void CTexturedLineRData::Render(
60 : Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
61 : Renderer::Backend::IVertexInputLayout* vertexInputLayout,
62 : const SOverlayTexturedLine& line, Renderer::Backend::IShaderProgram* shader)
63 : {
64 0 : if (!m_VB || !m_VBIndices)
65 0 : return; // might have failed to allocate
66 :
67 : // -- render main line quad strip ----------------------
68 :
69 0 : line.m_TextureBase->UploadBackendTextureIfNeeded(deviceCommandContext);
70 0 : line.m_TextureMask->UploadBackendTextureIfNeeded(deviceCommandContext);
71 :
72 0 : ENSURE(!m_VB->m_Owner->GetBuffer()->IsDynamic());
73 0 : ENSURE(!m_VBIndices->m_Owner->GetBuffer()->IsDynamic());
74 :
75 0 : deviceCommandContext->SetTexture(
76 0 : shader->GetBindingSlot(str_baseTex), line.m_TextureBase->GetBackendTexture());
77 0 : deviceCommandContext->SetTexture(
78 0 : shader->GetBindingSlot(str_maskTex), line.m_TextureMask->GetBackendTexture());
79 0 : deviceCommandContext->SetUniform(
80 0 : shader->GetBindingSlot(str_objectColor), line.m_Color.AsFloatArray());
81 :
82 0 : deviceCommandContext->SetVertexInputLayout(vertexInputLayout);
83 :
84 0 : deviceCommandContext->SetVertexBuffer(0, m_VB->m_Owner->GetBuffer(), 0);
85 :
86 0 : deviceCommandContext->SetIndexBuffer(m_VBIndices->m_Owner->GetBuffer());
87 0 : deviceCommandContext->DrawIndexed(m_VBIndices->m_Index, m_VBIndices->m_Count, 0);
88 :
89 0 : g_Renderer.GetStats().m_DrawCalls++;
90 0 : g_Renderer.GetStats().m_OverlayTris += m_VBIndices->m_Count/3;
91 : }
92 :
93 0 : void CTexturedLineRData::Update(const SOverlayTexturedLine& line)
94 : {
95 0 : m_VBIndices.Reset();
96 0 : m_VB.Reset();
97 :
98 0 : if (!line.m_SimContext)
99 : {
100 0 : debug_warn(L"[TexturedLineRData] No SimContext set for textured overlay line, cannot render (no terrain data)");
101 0 : return;
102 : }
103 :
104 0 : float v = 0.f;
105 0 : std::vector<SVertex> vertices;
106 0 : std::vector<u16> indices;
107 :
108 0 : const size_t n = line.m_Coords.size(); // number of line points
109 0 : bool closed = line.m_Closed;
110 :
111 0 : ENSURE(n >= 2); // minimum needed to avoid errors (also minimum value to make sense, can't draw a line between 1 point)
112 :
113 : // In each iteration, p1 is the position of vertex i, p0 is i-1, p2 is i+1.
114 : // To avoid slightly expensive terrain computations we cycle these around and
115 : // recompute p2 at the end of each iteration.
116 :
117 0 : CVector3D p0;
118 0 : CVector3D p1(line.m_Coords[0].X, 0, line.m_Coords[0].Y);
119 0 : CVector3D p2(line.m_Coords[1].X, 0, line.m_Coords[1].Y);
120 :
121 0 : if (closed)
122 : // grab the ending point so as to close the loop
123 0 : p0 = CVector3D(line.m_Coords[n - 1].X, 0, line.m_Coords[n - 1].Y);
124 : else
125 : // we don't want to loop around and use the direction towards the other end of the line, so create an artificial p0 that
126 : // extends the p2 -> p1 direction, and use that point instead
127 0 : p0 = p1 + (p1 - p2);
128 :
129 0 : bool p1floating = false;
130 0 : bool p2floating = false;
131 :
132 : // Compute terrain heights, clamped to the water height (and remember whether
133 : // each point was floating on water, for normal computation later)
134 :
135 : // TODO: if we ever support more than one water level per map, recompute this per point
136 0 : CmpPtr<ICmpWaterManager> cmpWaterManager(*line.m_SimContext, SYSTEM_ENTITY);
137 0 : float w = cmpWaterManager ? cmpWaterManager->GetExactWaterLevel(p0.X, p0.Z) : 0.f;
138 :
139 0 : const CTerrain& terrain = line.m_SimContext->GetTerrain();
140 :
141 0 : p0.Y = terrain.GetExactGroundLevel(p0.X, p0.Z);
142 0 : if (p0.Y < w)
143 0 : p0.Y = w;
144 :
145 0 : p1.Y = terrain.GetExactGroundLevel(p1.X, p1.Z);
146 0 : if (p1.Y < w)
147 : {
148 0 : p1.Y = w;
149 0 : p1floating = true;
150 : }
151 :
152 0 : p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z);
153 0 : if (p2.Y < w)
154 : {
155 0 : p2.Y = w;
156 0 : p2floating = true;
157 : }
158 :
159 0 : for (size_t i = 0; i < n; ++i)
160 : {
161 : // For vertex i, compute bisector of lines (i-1)..(i) and (i)..(i+1)
162 : // perpendicular to terrain normal
163 :
164 : // Normal is vertical if on water, else computed from terrain
165 0 : CVector3D norm;
166 0 : if (p1floating)
167 0 : norm = CVector3D(0, 1, 0);
168 : else
169 0 : norm = terrain.CalcExactNormal(p1.X, p1.Z);
170 :
171 0 : CVector3D b = ((p1 - p0).Normalized() + (p2 - p1).Normalized()).Cross(norm);
172 :
173 : // Adjust bisector length to match the line thickness, along the line's width
174 0 : float l = b.Dot((p2 - p1).Normalized().Cross(norm));
175 0 : if (fabs(l) > 0.000001f) // avoid unlikely divide-by-zero
176 0 : b *= line.m_Thickness / l;
177 :
178 : // Push vertices and indices for each quad in GL_TRIANGLES order. The two triangles of each quad are indexed using
179 : // the winding orders (BR, BL, TR) and (TR, BL, TL) (where BR is bottom-right of this iteration's quad, TR top-right etc).
180 0 : SVertex vertex1(p1 + b + norm * OverlayRenderer::OVERLAY_VOFFSET, CVector2D(0.f, v));
181 0 : SVertex vertex2(p1 - b + norm * OverlayRenderer::OVERLAY_VOFFSET, CVector2D(1.f, v));
182 0 : vertices.push_back(vertex1);
183 0 : vertices.push_back(vertex2);
184 :
185 0 : u16 vertexCount = static_cast<u16>(vertices.size());
186 0 : u16 index1 = vertexCount - 2; // index of vertex1 in this iteration (TR of this quad)
187 0 : u16 index2 = vertexCount - 1; // index of the vertex2 in this iteration (TL of this quad)
188 :
189 0 : if (i == 0)
190 : {
191 : // initial two vertices to continue building triangles from (n must be >= 2 for this to work)
192 0 : indices.push_back(index1);
193 0 : indices.push_back(index2);
194 : }
195 : else
196 : {
197 0 : u16 index1Prev = vertexCount - 4; // index of the vertex1 in the previous iteration (BR of this quad)
198 0 : u16 index2Prev = vertexCount - 3; // index of the vertex2 in the previous iteration (BL of this quad)
199 0 : ENSURE(index1Prev < vertexCount);
200 0 : ENSURE(index2Prev < vertexCount);
201 : // Add two corner points from last iteration and join with one of our own corners to create triangle 1
202 : // (don't need to do this if i == 1 because i == 0 are the first two ones, they don't need to be copied)
203 0 : if (i > 1)
204 : {
205 0 : indices.push_back(index1Prev);
206 0 : indices.push_back(index2Prev);
207 : }
208 0 : indices.push_back(index1); // complete triangle 1
209 :
210 : // create triangle 2, specifying the adjacent side's vertices in the opposite order from triangle 1
211 0 : indices.push_back(index1);
212 0 : indices.push_back(index2Prev);
213 0 : indices.push_back(index2);
214 : }
215 :
216 : // alternate V coordinate for debugging
217 0 : v = 1 - v;
218 :
219 : // cycle the p's and compute the new p2
220 0 : p0 = p1;
221 0 : p1 = p2;
222 0 : p1floating = p2floating;
223 :
224 : // if in closed mode, wrap around the coordinate array for p2 -- otherwise, extend linearly
225 0 : if (!closed && i == n-2)
226 : // next iteration is the last point of the line, so create an artificial p2 that extends the p0 -> p1 direction
227 0 : p2 = p1 + (p1 - p0);
228 : else
229 0 : p2 = CVector3D(line.m_Coords[(i + 2) % n].X, 0, line.m_Coords[(i + 2) % n].Y);
230 :
231 0 : p2.Y = terrain.GetExactGroundLevel(p2.X, p2.Z);
232 0 : if (p2.Y < w)
233 : {
234 0 : p2.Y = w;
235 0 : p2floating = true;
236 : }
237 : else
238 0 : p2floating = false;
239 : }
240 :
241 0 : if (closed)
242 : {
243 : // close the path
244 0 : if (n % 2 == 0)
245 : {
246 0 : u16 vertexCount = static_cast<u16>(vertices.size());
247 0 : indices.push_back(vertexCount - 2);
248 0 : indices.push_back(vertexCount - 1);
249 0 : indices.push_back(0);
250 :
251 0 : indices.push_back(0);
252 0 : indices.push_back(vertexCount - 1);
253 0 : indices.push_back(1);
254 : }
255 : else
256 : {
257 : // add two vertices to have the good UVs for the last quad
258 0 : SVertex vertex1(vertices[0].m_Position, CVector2D(0.f, 1.f));
259 0 : SVertex vertex2(vertices[1].m_Position, CVector2D(1.f, 1.f));
260 0 : vertices.push_back(vertex1);
261 0 : vertices.push_back(vertex2);
262 :
263 0 : u16 vertexCount = static_cast<u16>(vertices.size());
264 0 : indices.push_back(vertexCount - 4);
265 0 : indices.push_back(vertexCount - 3);
266 0 : indices.push_back(vertexCount - 2);
267 :
268 0 : indices.push_back(vertexCount - 2);
269 0 : indices.push_back(vertexCount - 3);
270 0 : indices.push_back(vertexCount - 1);
271 : }
272 : }
273 : else
274 : {
275 : // Create start and end caps. On either end, this is done by taking the centroid between the last and second-to-last pair of
276 : // vertices that was generated along the path (i.e. the vertex1's and vertex2's from above), taking a directional vector
277 : // between them, and drawing the line cap in the plane given by the two butt-end corner points plus said vector.
278 0 : std::vector<u16> capIndices;
279 0 : std::vector<SVertex> capVertices;
280 :
281 : // create end cap
282 0 : CreateLineCap(
283 : line,
284 : // the order of these vertices is important here, swapping them produces caps at the wrong side
285 0 : vertices[vertices.size()-2].m_Position, // top-right vertex of last quad
286 0 : vertices[vertices.size()-1].m_Position, // top-left vertex of last quad
287 : // directional vector between centroids of last vertex pair and second-to-last vertex pair
288 0 : (Centroid(vertices[vertices.size()-2], vertices[vertices.size()-1]) - Centroid(vertices[vertices.size()-4], vertices[vertices.size()-3])).Normalized(),
289 0 : line.m_EndCapType,
290 : capVertices,
291 : capIndices
292 : );
293 :
294 0 : for (unsigned i = 0; i < capIndices.size(); i++)
295 0 : capIndices[i] += static_cast<u16>(vertices.size());
296 :
297 0 : vertices.insert(vertices.end(), capVertices.begin(), capVertices.end());
298 0 : indices.insert(indices.end(), capIndices.begin(), capIndices.end());
299 :
300 0 : capIndices.clear();
301 0 : capVertices.clear();
302 :
303 : // create start cap
304 0 : CreateLineCap(
305 : line,
306 : // the order of these vertices is important here, swapping them produces caps at the wrong side
307 0 : vertices[1].m_Position,
308 0 : vertices[0].m_Position,
309 : // directional vector between centroids of first vertex pair and second vertex pair
310 0 : (Centroid(vertices[1], vertices[0]) - Centroid(vertices[3], vertices[2])).Normalized(),
311 0 : line.m_StartCapType,
312 : capVertices,
313 : capIndices
314 : );
315 :
316 0 : for (unsigned i = 0; i < capIndices.size(); i++)
317 0 : capIndices[i] += static_cast<u16>(vertices.size());
318 :
319 0 : vertices.insert(vertices.end(), capVertices.begin(), capVertices.end());
320 0 : indices.insert(indices.end(), capIndices.begin(), capIndices.end());
321 : }
322 :
323 0 : if (vertices.empty() || indices.empty())
324 0 : return;
325 :
326 : // Indices for triangles, so must be multiple of 3.
327 0 : ENSURE(indices.size() % 3 == 0);
328 :
329 0 : m_BoundingBox = CBoundingBoxAligned();
330 0 : for (const SVertex& vertex : vertices)
331 0 : m_BoundingBox += vertex.m_Position;
332 :
333 0 : m_VB = g_VBMan.AllocateChunk(
334 0 : sizeof(SVertex), vertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false);
335 : // Allocation might fail (e.g. due to too many vertices).
336 0 : if (m_VB)
337 : {
338 : // Copy data into backend buffer.
339 0 : m_VB->m_Owner->UpdateChunkVertices(m_VB.Get(), &vertices[0]);
340 :
341 0 : for (size_t k = 0; k < indices.size(); ++k)
342 0 : indices[k] += static_cast<u16>(m_VB->m_Index);
343 :
344 0 : m_VBIndices = g_VBMan.AllocateChunk(
345 0 : sizeof(u16), indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false);
346 0 : if (m_VBIndices)
347 0 : m_VBIndices->m_Owner->UpdateChunkVertices(m_VBIndices.Get(), &indices[0]);
348 : }
349 :
350 : }
351 :
352 0 : void CTexturedLineRData::CreateLineCap(const SOverlayTexturedLine& line, const CVector3D& corner1, const CVector3D& corner2,
353 : const CVector3D& lineDirectionNormal, SOverlayTexturedLine::LineCapType endCapType, std::vector<SVertex>& verticesOut,
354 : std::vector<u16>& indicesOut)
355 : {
356 0 : if (endCapType == SOverlayTexturedLine::LINECAP_FLAT)
357 0 : return; // no action needed, this is the default
358 :
359 : // When not in closed mode, we've created artificial points for the start- and endpoints that extend the line in the
360 : // direction of the first and the last segment, respectively. Thus, we know both the start and endpoints have perpendicular
361 : // butt endings, i.e. the end corner vertices on either side of the line extend perpendicularly from the segment direction.
362 : // That is to say, when viewed from the top, we will have something like
363 : // .
364 : // this: and not like this: /|
365 : // ----+ / |
366 : // | / .
367 : // | /
368 : // ----+ /
369 : //
370 :
371 0 : int roundCapPoints = 8; // amount of points to sample along the semicircle for rounded caps (including corner points)
372 0 : float radius = line.m_Thickness;
373 :
374 0 : CVector3D centerPoint = (corner1 + corner2) * 0.5f;
375 0 : SVertex centerVertex(centerPoint, CVector2D(0.5f, 0.5f));
376 0 : u16 indexOffset = static_cast<u16>(verticesOut.size()); // index offset in verticesOut from where we start adding our vertices
377 :
378 0 : switch (endCapType)
379 : {
380 0 : case SOverlayTexturedLine::LINECAP_SHARP:
381 : {
382 0 : roundCapPoints = 3; // creates only one point directly ahead
383 0 : radius *= 1.5f; // make it a bit sharper (note that we don't use the radius for the butt-end corner points so it should be ok)
384 0 : centerVertex.m_UV.X = 0.480f; // slight visual correction to make the texture match up better at the corner points
385 : }
386 : FALLTHROUGH;
387 0 : case SOverlayTexturedLine::LINECAP_ROUND:
388 : {
389 : // Draw a rounded line cap in the 3D plane of the line specified by the two corner points and the normal vector of the
390 : // line's direction. The terrain normal at the centroid between the two corner points is perpendicular to this plane.
391 : // The way this works is by taking a vector from the corner points' centroid to one of the corner points (which is then
392 : // of radius length), and rotate it around the terrain normal vector in that centroid. This will rotate the vector in
393 : // the line's plane, producing the desired rounded cap.
394 :
395 : // To please OpenGL's winding order, this angle needs to be negated depending on whether we start rotating from
396 : // the (center -> corner1) or (center -> corner2) vector. For the (center -> corner2) vector, we apparently need to use
397 : // the negated angle.
398 0 : float stepAngle = -(float)(M_PI/(roundCapPoints-1));
399 :
400 : // Push the vertices in triangle fan order (easy to generate GL_TRIANGLES indices for afterwards)
401 : // Note that we're manually adding the corner vertices instead of having them be generated by the rotating vector.
402 : // This is because we want to support an overly large radius to make the sharp line ending look sharper.
403 0 : verticesOut.push_back(centerVertex);
404 0 : verticesOut.push_back(SVertex(corner2, CVector2D()));
405 :
406 : // Get the base vector that we will incrementally rotate in the cap plane to produce the radial sample points.
407 : // Normally corner2 - centerPoint would suffice for this since it is of radius length, but we want to support custom
408 : // radii to support tuning the 'sharpness' of sharp end caps (see above)
409 0 : CVector3D rotationBaseVector = (corner2 - centerPoint).Normalized() * radius;
410 : // Calculate the normal vector of the plane in which we're going to be drawing the line cap. This is the vector that
411 : // is perpendicular to both baseVector and the 'lineDirectionNormal' vector indicating the direction of the line.
412 : // Note that we shouldn't use terrain->CalcExactNormal() here because if the line is being rendered on top of water,
413 : // then CalcExactNormal will return the normal vector of the terrain that's underwater (which can be quite funky).
414 0 : CVector3D capPlaneNormal = lineDirectionNormal.Cross(rotationBaseVector).Normalized();
415 :
416 0 : for (int i = 1; i < roundCapPoints - 1; ++i)
417 : {
418 : // Rotate the centerPoint -> corner vector by i*stepAngle radians around the cap plane normal at the center point.
419 0 : CQuaternion quatRotation;
420 0 : quatRotation.FromAxisAngle(capPlaneNormal, i * stepAngle);
421 0 : CVector3D worldPos3D = centerPoint + quatRotation.Rotate(rotationBaseVector);
422 :
423 : // Let v range from 0 to 1 as we move along the semi-circle, keep u fixed at 0 (i.e. curve the left vertical edge
424 : // of the texture around the edge of the semicircle)
425 0 : float u = 0.f;
426 0 : float v = Clamp((i / static_cast<float>(roundCapPoints - 1)), 0.f, 1.f); // pos, u, v
427 0 : verticesOut.push_back(SVertex(worldPos3D, CVector2D(u, v)));
428 : }
429 :
430 : // connect back to the other butt-end corner point to complete the semicircle
431 0 : verticesOut.push_back(SVertex(corner1, CVector2D(0.f, 1.f)));
432 :
433 : // now push indices in GL_TRIANGLES order; vertices[indexOffset] is the center vertex, vertices[indexOffset + 1] is the
434 : // first corner point, then a bunch of radial samples, and then at the end we have the other corner point again. So:
435 0 : for (int i=1; i < roundCapPoints; ++i)
436 : {
437 0 : indicesOut.push_back(indexOffset); // center vertex
438 0 : indicesOut.push_back(indexOffset + i);
439 0 : indicesOut.push_back(indexOffset + i + 1);
440 0 : }
441 : }
442 0 : break;
443 :
444 0 : case SOverlayTexturedLine::LINECAP_SQUARE:
445 : {
446 : // Extend the (corner1 -> corner2) vector along the direction normal and draw a square line ending consisting of
447 : // three triangles (sort of like a triangle fan)
448 : // NOTE: The order in which the vertices are pushed out determines the visibility, as they
449 : // are rendered only one-sided; the wrong order of vertices will make the cap visible only from the bottom.
450 0 : verticesOut.push_back(centerVertex);
451 0 : verticesOut.push_back(SVertex(corner2, CVector2D()));
452 0 : verticesOut.push_back(SVertex(corner2 + (lineDirectionNormal * (line.m_Thickness)), CVector2D(0.f, 0.33333f))); // extend butt corner point 2 along the normal vector
453 0 : verticesOut.push_back(SVertex(corner1 + (lineDirectionNormal * (line.m_Thickness)), CVector2D(0.f, 0.66666f))); // extend butt corner point 1 along the normal vector
454 0 : verticesOut.push_back(SVertex(corner1, CVector2D(0.f, 1.0f))); // push butt corner point 1
455 :
456 0 : for (int i=1; i < 4; ++i)
457 : {
458 0 : indicesOut.push_back(indexOffset); // center point
459 0 : indicesOut.push_back(indexOffset + i);
460 0 : indicesOut.push_back(indexOffset + i + 1);
461 0 : }
462 : }
463 0 : break;
464 :
465 0 : default:
466 0 : break;
467 : }
468 :
469 : }
470 :
471 0 : bool CTexturedLineRData::IsVisibleInFrustum(const CFrustum& frustum) const
472 : {
473 0 : return frustum.IsBoxVisible(m_BoundingBox);
474 3 : }
|