Cogs.Core
SpriteRenderSystem.cpp
1#include "SpriteRenderSystem.h"
2
3#include "Foundation/Logging/Logger.h"
4#include "Rendering/ICapabilities.h"
5
6#include "Context.h"
7
8#include "Resources/MaterialManager.h"
9#include "Resources/TextureManager.h"
10#include "Resources/MeshManager.h"
11#include "Resources/DefaultMaterial.h"
12
13#include "Scene/PickingFlags.h"
14#include "Scene/RayPick.h"
15
16#include "Systems/Core/CameraSystem.h"
17
18namespace
19{
20 const Cogs::Logging::Log logger = Cogs::Logging::getLogger("SpriteRenderSystem");
21}
22
23Cogs::Core::SpriteRenderSystem::SpriteRenderSystem(Memory::Allocator * allocator, SizeType capacity) :
24 ComponentSystemWithDataPool(allocator, capacity)
25{
26
27}
28
30{
32
33 expandSpriteGeometry = !context->device->getCapabilities()->getDeviceCapabilities().GeometryShaders;
34
35 if (expandSpriteGeometry) {
36 spriteMaterial = context->materialManager->loadMaterial("Materials/SpriteNoGsMaterial.material");
37 } else {
38 spriteMaterial = context->materialManager->loadMaterial("Materials/SpriteMaterial.material");
39 }
40 context->materialManager->processLoading();
42 spriteMaterial->options.depthWriteEnabled = false;
43
44 registerSpriteMaterial(spriteMaterial);
45
46 spriteBatches.reserve(1024);
47}
48
50{
51 currentBatch = nullptr;
52 spriteBatches.clear();
53
54 for (auto & m : materials) {
55 m.freeInstances.insert(m.freeInstances.end(), m.frameInstances.begin(), m.frameInstances.end());
56 m.frameInstances.clear();
57 }
58
59 // Reset all batch indexes to ensure no invalid sprites are considered
60 // active for picking/bounds/rendering.
61 for (auto & sprite : pool) {
62 auto & spriteData = getData(&sprite);
63 spriteData.batchIndex = NoBatchIndex;
64 }
65
66 freeMeshes.insert(freeMeshes.end(), frameMeshes.begin(), frameMeshes.end());
67 frameMeshes.clear();
68}
69
71{
72 for (auto & sprite : pool) {
73 auto & spriteData = getData(&sprite);
74
75 if (!sprite.isVisible()) {
76 spriteData.batchIndex = NoBatchIndex;
77 continue;
78 }
79
80 if (spriteData.batchIndex == NoBatchIndex) continue;
81
82 auto batch = getSpriteBatch(spriteData.batchIndex);
83
84 auto spritePosition = glm::vec3(batch->transform * glm::vec4(spriteData.position, 1));
85
86 const glm::vec3 kEpsilon = glm::vec3(0.01f);
87 spriteData.boundingBox = { spritePosition - kEpsilon, spritePosition + kEpsilon };
88 }
89
90 base::postUpdate(context);
91}
92
94{
95 ComponentHandle componentHandle = base::createComponent();
96
97 if(pool.size() == 1) {
98 assert(picker == nullptr);
99 picker = new SpritePicker(this);
100 context->rayPicking->addPickable(picker);
101
102 LOG_DEBUG(logger, "Registered sprint picker extension");
103 }
104
105 return componentHandle;
106}
107
109{
110 base::destroyComponent(component);
111
112 if (pool.size() == 0) {
113 assert(picker != nullptr);
114 context->rayPicking->removePickable(picker);
115 delete picker;
116 picker = nullptr;
117
118 LOG_DEBUG(logger, "Last component destroyed, unregistered sprite picker extension");
119 }
120}
121
122size_t Cogs::Core::SpriteRenderSystem::registerSpriteMaterial(MaterialHandle material)
123{
124 materials.resize(materials.size() + 1);
125 auto & spriteMaterial = materials.back();
126
127 spriteMaterial.material = material;
128
129 if (material->isDefaultMaterial()) {
130 spriteMaterial.textureKey = DefaultMaterial::DiffuseMap;
131 spriteMaterial.colorKey = DefaultMaterial::DiffuseColor;
132 } else {
133 spriteMaterial.textureKey = material->getTextureKey("spriteTexture");
134
135 if (spriteMaterial.textureKey == NoProperty) {
136 LOG_ERROR(logger, "Sprite material does not contain texture property.");
137 return NoSpriteMaterial;
138 }
139
140 spriteMaterial.colorKey = material->getVec4Key("spriteColor");
141
142 if (spriteMaterial.colorKey == NoProperty) {
143 LOG_WARNING(logger, "Sprite material missing color property.");
144 }
145
146 spriteMaterial.alwaysOnTopKey = material->getBoolKey("alwaysOnTop");
147
148 if (spriteMaterial.alwaysOnTopKey == NoProperty) {
149 LOG_WARNING(logger, "Sprite material missing 'alwaysOnTop' property.");
150 }
151 }
152
153 return materials.size() - 1;
154}
155
156Cogs::Core::SpriteBatch * Cogs::Core::SpriteRenderSystem::begin(SpriteMaterialKey materialKey, const StringView & permutation, TextureHandle texture, glm::vec4 color, bool alwaysOnTop)
157{
158 if (!currentBatch || currentBatch->count != 0) {
159 if (currentBatch && !currentBatch->count) {
160 LOG_WARNING(logger, "Empty sprite batch detected.");
161 }
162
163 spriteBatches.resize(spriteBatches.size() + 1);
164 currentBatch = &spriteBatches.back();
165 currentBatch->index = spriteBatches.size() - 1;
166 }
167
168 updateBatchMesh();
169
170 if (materialKey == NoSpriteMaterial) {
171 LOG_WARNING(logger, "Invalid sprite material key, falling back to default.");
172
173 materialKey = DefaultSpriteMaterial;
174 }
175
176 auto & spriteMaterial = materials[materialKey];
177
178 if (spriteMaterial.freeInstances.size()) {
179 currentBatch->material = spriteMaterial.freeInstances.back();
180 spriteMaterial.freeInstances.pop_back();
181 currentBatch->material->reset();
182 } else {
183 currentBatch->material = context->materialInstanceManager->createMaterialInstance(spriteMaterial.material);
184 }
185
186 spriteMaterial.frameInstances.push_back(currentBatch->material);
187
188 if (!currentBatch->material->isDefaultMaterial()) {
189 currentBatch->material->setPermutation(permutation);
190 } else {
191 color = glm::vec4(glm::vec3(color) * color.a, color.a);
192 currentBatch->material->setMaterialFlag(MaterialFlags::Sprite);
193 currentBatch->material->setBoolProperty(DefaultMaterial::EnableLighting, false);
194 currentBatch->material->options.blendMode = BlendMode::Blend;
195 currentBatch->material->options.depthWriteEnabled = false;
196 }
197
198 currentBatch->material->setTextureProperty(spriteMaterial.textureKey, texture);
199
200 if (spriteMaterial.colorKey != NoProperty) {
201 currentBatch->material->setVec4Property(spriteMaterial.colorKey, color);
202 }
203
204 if (spriteMaterial.alwaysOnTopKey != NoProperty) {
205 currentBatch->material->setBoolProperty(spriteMaterial.alwaysOnTopKey, alwaysOnTop);
206 }
207
208 currentBatch->flags |= alwaysOnTop ? RenderItemFlags::AlwaysOnTop : RenderItemFlags::None;
209
210 return currentBatch;
211}
212
213Cogs::Core::SpriteBatch * Cogs::Core::SpriteRenderSystem::begin(SpriteMaterialKey material,
214 const StringView & postfix,
215 PositionMode positionMode,
216 SizeMode sizeMode,
217 TextureHandle texture,
218 glm::vec4 color,
219 bool alwaysOnTop)
220{
221 const auto permutation = getPermutation(positionMode, sizeMode).to_string() + postfix.to_string();
222
223 auto batch = begin(material, permutation, texture, color, alwaysOnTop);
224
225 batch->positionMode = positionMode;
226 batch->sizeMode = sizeMode;
227
228 return batch;
229}
230
231Cogs::Core::SpriteBatch * Cogs::Core::SpriteRenderSystem::begin(SpriteMaterialKey material, TextureHandle texture, glm::vec4 color)
232{
233 return begin(material, "", texture, color);
234}
235
236Cogs::Core::SpriteBatch * Cogs::Core::SpriteRenderSystem::begin(TextureHandle texture, glm::vec4 color)
237{
238 return begin(DefaultSpriteMaterial, texture, color);
239}
240
241void Cogs::Core::SpriteRenderSystem::addSprite(SpriteRenderComponent * spriteRenderer, glm::vec3 position, glm::vec2 size, glm::vec4 texCoords)
242{
243 addSprite(spriteRenderer, position, glm::vec2(0, 0), size, texCoords);
244}
245
246void Cogs::Core::SpriteRenderSystem::addSprite(SpriteRenderComponent * spriteRenderer, glm::vec3 position, glm::vec2 offset, glm::vec2 size, glm::vec4 texCoords)
247{
248 if (!currentBatch) {
249 LOG_ERROR(logger, "Cannot add sprite without active batch.");
250 return;
251 }
252
253 if (!spriteRenderer) {
254 LOG_ERROR(logger, "Cannot add sprite without SpriteRenderComponent.");
255 return;
256 }
257
258 if (!spriteRenderer->isVisible()) return;
259
260 if (!expandSpriteGeometry) {
261 currentBatch->count++;
262
263 const size_t spriteIndex = currentBatch->start + currentBatch->count - 1;
264
265 auto & spriteData = getData(spriteRenderer);
266 spriteData.batchIndex = currentBatch->index;
267 spriteData.spriteIndex = spriteIndex;
268
269 spriteData.position = position;
270 spriteData.offset = offset;
271 spriteData.size = size;
272
273 if (currentBatch->count == 1) {
274 // Get LoD data from the first sprite so we have something consistent
275 // for the batch.
276 currentBatch->lod = spriteRenderer->lod;
277 }
278
279 auto mesh = currentBatch->mesh.resolve();
280
281 const size_t meshCount = currentBatch->start + currentBatch->count;
282 auto vertexData = mesh->map<SpriteVertex>(
283 VertexDataType::Interleaved0,
284 SpriteVertex::getVertexFormat(),
285 0,
286 meshCount < kMinBatchSize ? kMinBatchSize : meshCount);
287
288 vertexData[spriteIndex].position = glm::vec4(position, 1);
289 vertexData[spriteIndex].parameters = glm::vec4(size, offset);
290 vertexData[spriteIndex].texCoords = texCoords;
291
292 mesh->setCount(meshCount);
293 } else {
294 currentBatch->expanded = true;
295
296 const size_t spriteIndex = currentBatch->start + currentBatch->count;
297 currentBatch->count += 6;
298
299 auto & spriteData = getData(spriteRenderer);
300 spriteData.batchIndex = currentBatch->index;
301 spriteData.spriteIndex = spriteIndex/6;
302
303 spriteData.position = position;
304 spriteData.offset = offset;
305 spriteData.size = size;
306
307 if (currentBatch->count == 6) {
308 // Get LoD data from the first sprite so we have something consistent
309 // for the batch.
310 currentBatch->lod = spriteRenderer->lod;
311 }
312
313 auto mesh = currentBatch->mesh.resolve();
314
315 const size_t meshCount = currentBatch->start + currentBatch->count;
316 auto vertexData = mesh->map<SpriteVertex>(
317 VertexDataType::Interleaved0,
318 SpriteVertex::getVertexFormat(),
319 0,
320 meshCount < kMinBatchSize ? kMinBatchSize : meshCount);
321
322 const float indices[6] = { 0, 1, 2, 2, 1, 3 };
323 for (unsigned i = 0; i < 6; i++) {
324 vertexData[spriteIndex + i].position = glm::vec4(position, indices[i]);
325 vertexData[spriteIndex + i].parameters = glm::vec4(size, offset);
326 vertexData[spriteIndex + i].texCoords = texCoords;
327 }
328
329 mesh->setCount(meshCount);
330 }
331}
332
333const Cogs::Core::SpriteBatch * Cogs::Core::SpriteRenderSystem::getSpriteBatch(SpriteBatchIndex index) const
334{
335 if (index >= spriteBatches.size()) return nullptr;
336
337 return &spriteBatches[index];
338}
339
340std::span<const Cogs::Core::SpriteBatch> Cogs::Core::SpriteRenderSystem::getSpriteBatches() const
341{
342 return spriteBatches;
343}
344
345Cogs::StringView Cogs::Core::SpriteRenderSystem::getPermutation(PositionMode positionMode, SizeMode sizeMode)
346{
347 if (sizeMode == SizeMode::Relative) {
348 switch (positionMode)
349 {
351 return "NoTransformRelativeSize";
353 return "PixelTransformRelativeSize";
355 case PositionMode::Local:
356 return "ViewTransformRelativeSize";
357 default:
358 break;
359 }
360 } else {
361 switch (positionMode)
362 {
364 return "PixelTransformPixelSize";
366 case PositionMode::Local:
367 return "ViewTransformPixelSize";
368 default:
369 break;
370 }
371 }
372
373 return "Default";
374}
375
376void Cogs::Core::SpriteRenderSystem::updateBatchMesh()
377{
378 const size_t kMeshMinBatchCount = 1024;
379
380 if (freeMeshes.size()) {
381 currentBatch->mesh = freeMeshes.back();
382 freeMeshes.pop_back();
383 frameMeshes.push_back(currentBatch->mesh);
384 } else {
385 for (auto & m : frameMeshes) {
386 if (m->getCount() < kMeshMinBatchCount) {
387 currentBatch->mesh = m;
388 currentBatch->start = m->getCount();
389 break;
390 }
391 }
392
393 if (!currentBatch->mesh) {
394 currentBatch->mesh = context->meshManager->create();
395
396 currentBatch->mesh->primitiveType = expandSpriteGeometry ? PrimitiveType::TriangleList : PrimitiveType::PointList;
397 currentBatch->mesh->setName("SpriteMesh");
398 frameMeshes.push_back(currentBatch->mesh);
399 }
400 }
401}
402
403
404Cogs::Core::SpritePicker::SpritePicker(SpriteRenderSystem* spriteSystem)
405 : spriteSystem(spriteSystem)
406{
407}
408
410 const CameraComponent& camera,
411 const glm::vec2& normPosition,
412 float /*rayLength*/,
413 float /*radius*/,
414 PickingFlags pickingFlags,
415 PicksReturned returnFlag,
416 const RayPicking::RayPickFilter& filter,
417 std::vector<RayPicking::RayPickHit>& hits)
418{
420 // Picking sprites is disabled
421 return {};
422 }
423
424 const bool returnChildEntity = (pickingFlags & PickingFlags::ReturnChildEntity) == PickingFlags::ReturnChildEntity;
425
426 const CameraData& cameraData = context->cameraSystem->getData(&camera);
427 const glm::mat4 invProjection = glm::inverse(cameraData.rawViewProjection);
428
429 bool hitSomething = false;
430
431 for (const SpriteRenderComponent& sprite : spriteSystem->pool) {
432 if (!sprite.isVisibleInLayer(filter.layerMask) ||
433 !sprite.isPickable() ||
434 sprite.lod.currentLod != sprite.lod.selectedLod ||
435 filter.isUnwantedType(sprite)) {
436 continue;
437 }
438
439 const SpriteRenderData& data = context->spriteRenderSystem->getData(&sprite);
440 const SpriteBatch* batch = context->spriteRenderSystem->getSpriteBatch(data.batchIndex);
441 if (!batch || batch->mesh == MeshHandle::NoHandle || batch->count == 0) {
442 continue;
443 }
444
445 const size_t meshCount = batch->start + batch->count;
446 auto meshData = batch->mesh->mapReadOnly<SpriteVertex>(VertexDataType::Interleaved0,
447 SpriteVertex::getVertexFormat(),
448 0,
449 meshCount < kMinBatchSize ? kMinBatchSize : meshCount);
450 if (!meshData.isValid()) {
451 continue;
452 }
453
454 // Need only check first vertex. SpriteRenderSystem stores same position in every index.
455 const SpriteVertex& spriteVertex = meshData[batch->start];
456
457 const glm::vec3 position = spriteVertex.position;
458 const glm::vec2 offset = glm::vec2(spriteVertex.parameters.z, spriteVertex.parameters.w);
459 const glm::vec2 size = glm::vec2(spriteVertex.parameters.x, spriteVertex.parameters.y);
460
461 glm::vec4 normPos = glm::vec4(0, 0, 0, 1);
462 glm::vec2 normOffset = offset / (0.5f * cameraData.viewportSize);
463
464 if (batch->positionMode == PositionMode::Pixels) {
465 normPos = glm::vec4(glm::vec2(position) / (0.5f * cameraData.viewportSize) - glm::vec2(1, 1) + normOffset, position.z, 1);
466 }
467 else if (batch->positionMode == PositionMode::Relative) {
468 normPos = glm::vec4(glm::vec2(position) + normOffset, position.z, 1);
469 }
470 else {
471 const glm::vec3 worldPos = glm::vec3(batch->transform * glm::vec4(position, 1));
472 normPos = cameraData.rawViewProjection * glm::vec4(worldPos, 1);
473 normPos /= normPos.w;
474 normPos = glm::vec4(glm::vec2(normPos) + normOffset, normPos.z, 1);
475 }
476
477 const glm::vec2 normSize = size / cameraData.viewportSize;
478
479 const float xMin = normPos.x - glm::abs(normSize.x);
480 const float xMax = normPos.x + glm::abs(normSize.x);
481 const float yMin = normPos.y - glm::abs(normSize.y);
482 const float yMax = normPos.y + glm::abs(normSize.y);
483
484 if (normPosition.x > xMin && normPosition.x < xMax && normPosition.y > yMin && normPosition.y < yMax) {
485 glm::vec4 worldPos = invProjection * glm::vec4(normPosition.x, normPosition.y, normPos.z, 1.0f);
486 worldPos /= worldPos.w;
487
488 glm::vec4 clipPos = cameraData.rawViewProjection * glm::vec4(glm::vec3(worldPos), 1.f);
489 if ((-clipPos.w <= clipPos.z) && (clipPos.z <= clipPos.w)) {
490
491 const glm::vec4 viewPos= cameraData.viewMatrix * worldPos;
492 const float viewDist = -viewPos.z;
493
494 if (returnFlag == PicksReturned::Closest && !hits.empty()) {
495 if (viewDist < hits[0].distance) {
496 hits[0] = {sprite, returnChildEntity, position, viewDist};
497 hitSomething = true;
498 }
499 // else, the intersection we found is further, so don't do anything
500 }
501 else {
502 hits.emplace_back(sprite, returnChildEntity, position, viewDist);
503 hitSomething = true;
504 }
505 }
506 }
507 }
508
509 if (returnFlag == PicksReturned::AllSorted) {
510 std::sort(hits.begin(), hits.end());
511 }
512 return hitSomething;
513}
Context * context
Pointer to the Context instance the system lives in.
void postUpdate()
Perform post update logic in the system.
virtual void initialize(Context *context)
Initialize the system.
void preUpdate()
Run the pre-update method of the system.
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
constexpr bool isVisibleInLayer(RenderLayers layerMask) const
Check if the entity should be visible in the given layer mask.
constexpr bool isPickable() const
Check if the entity is pickable or not.
bool pickCamera(Context *context, const CameraComponent &camera, const glm::vec2 &normPosition, float, float, PickingFlags pickingFlags, PicksReturned returnFlag, const RayPicking::RayPickFilter &filter, std::vector< RayPicking::RayPickHit > &hits) override
Do a ray pick from a normalized screen space position in the camera direction and return all hits.
void initialize(Context *context) override
Initialize the system.
ComponentHandle createComponent() override
void destroyComponent(ComponentHandle component) override
Log implementation class.
Definition: LogManager.h:139
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
PositionMode
Positioning mode.
@ Relative
Position given in normalized device coordinates, with [-1, -1] corresponding to the lower left of the...
@ Pixels
Position given in screen pixels ranging from [0, 0] in the lower left corner to [viewport....
@ World
Position given in world space coordinates.
PicksReturned
  * Options for returning picking hits.
Definition: PickingFlags.h:40
@ Closest
Return just the closest hit.
@ AllSorted
Return all hits sorted based on distance from the ray start, closest first.
PickingFlags
Options for COGS picking.
Definition: PickingFlags.h:12
@ ReturnChildEntity
Return ID if sub-entity picked, not set: return root parent entity.
@ PickSprites
Check picking for entities with SpriteRenderComponent. I.e. Text, Annotation, Billboard,...
@ Blend
Render with regular alpha blending.
SizeMode
Sizing mode.
@ Relative
Size given in normalized device coordinates.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
Handle to a Component instance.
Definition: Component.h:67
Contains data describing a Camera instance and its derived data structured such as matrix data and vi...
Definition: CameraSystem.h:67
@ Sprite
The material is used for sprite rendering only.
Definition: Material.h:62
BlendMode blendMode
Blend mode to use when rendering geometry with the material instance.
bool depthWriteEnabled
If depth writes should be enabled rendering the geometry with the material instance.
MaterialOptions options
Material rendering options.
Definition: Material.h:383
MappedStreamReadOnly< Element > mapReadOnly(const VertexDataType::EVertexDataType type, VertexFormatHandle format, const size_t start, const size_t count)
Maps the data stream corresponding to the given type for read-only access.
Definition: Mesh.h:857
bool isUnwantedType(const ComponentModel::Component &comp) const
Helper function used to determine if a given component belongs to an accepted entity type.
Definition: RayPick.cpp:187
RenderLayers layerMask
Limit picking to the specified render layers. Pick all layers by default.
Definition: RayPick.h:70
uint8_t currentLod
The assigned LoD of the current component.
uint8_t selectedLod
The selected LoD of the composite entity.
static const ResourceHandle_t NoHandle
Handle representing a default (or none if default not present) resource.
@ PointList
List of points.
Definition: Common.h:124
@ TriangleList
List of triangles.
Definition: Common.h:116