Cogs.Core
BadgeSetPicker.cpp
1#include "BadgeSetPicker.h"
2#include "BadgeSetSystem.h"
3
4#include "Context.h"
5#include "ViewContext.h"
6#include "Components/Core/SceneComponent.h"
7#include "Components/Core/TransformComponent.h"
8#include "Systems/Core/TransformSystem.h"
9#include "Scene/RayPick.h"
10#include "Services/DPIService.h"
11#include "Resources/Mesh.h"
12#include "Resources/Texture.h"
13#include "Systems/Core/CameraSystem.h"
14#include "Utilities/Math.h"
15#include "Systems/ComponentSystem.h"
16
17#include "Rendering/IGraphicsDevice.h"
18#include "Rendering/ICapabilities.h"
19
20#include "Foundation/Logging/Logger.h"
21
22namespace
23{
24 using namespace Cogs::Core;
25
26 Cogs::Logging::Log logger = Cogs::Logging::getLogger("BadgeSetPicker");
27
28 struct BoundingBox
29 {
30 glm::vec2 min;
31 glm::vec2 max;
32 BoundingBox (float minX, float minY, float maxX, float maxY) {
33 min.x = minX;
34 min.y = minY;
35 max.x = maxX;
36 max.y = maxY;
37 }
38
39 bool isPointWithin(const glm::vec2 &mouse) const {
40 return mouse.x >= min.x && mouse.x <= max.x && mouse.y >= min.y && mouse.y <= max.y;
41 }
42 };
43
44 struct RecordHit
45 {
46 std::vector<RayPicking::RayPickHit>& hits;
47 const BadgeSetComponent& comp;
48 RayPicking::Ordinal ordinal;
49 const RenderComponent& renderComp;
50 const glm::mat4& worldFromLocal;
51 const float rayFromLocalScale;
52 bool returnClosest;
53 bool returnChildEntity;
54 bool hitSomething = false;
55
56 void operator()(const glm::vec4& posLocal, const float distanceLocal, size_t index) {
57 glm::vec4 posWorld = worldFromLocal * posLocal;
58 const float distanceWorld = rayFromLocalScale * distanceLocal;
59 if (returnClosest && !hits.empty()) {
60 if (hits[0].isBehind(ordinal, distanceWorld)) {
61 hits[0] = { renderComp, returnChildEntity, glm::vec3(posWorld), ordinal, distanceWorld, glm::vec2(static_cast<float>(index)) };
62 hitSomething = true;
63 }
64 // else, the intersection we found is further, so don't do anything
65 }
66 else {
67 hits.emplace_back(renderComp, returnChildEntity, glm::vec3(posWorld), ordinal, distanceWorld, glm::vec2(static_cast<float>(index)));
68 hitSomething = true;
69 }
70 }
71
72 };
73
75 constexpr int styleMaskOpaque = 1;
77 constexpr int styleMaskTransparent = 0;
79 constexpr int styleMaskUnknown = -1;
80
93 int sampleStyleMask(const BadgeSetComponent& comp, size_t badgeIndex, const glm::vec2& quadUV)
94 {
95 const std::vector<int>& mask = comp.masks;
96 if (mask.size() < 3) {
97 return styleMaskUnknown;
98 }
99
100 const int width = mask[0];
101 const int height = mask[1];
102 const int layers = mask[2];
103 if (width <= 0 || height <= 0 || layers <= 0) {
104 return styleMaskUnknown;
105 }
106
107 // The style array layer for this badge is its style index (see BadgeSetSystem).
108 int layer = 0;
109 if (badgeIndex < comp.styles.size()) {
110 const int styleIndex = comp.styles[badgeIndex];
111 if (styleIndex < 0 || styleIndex >= layers) {
112 return styleMaskUnknown;
113 }
114 layer = styleIndex;
115 }
116
117 const size_t wordsPerLayer = (static_cast<size_t>(width) * static_cast<size_t>(height) + 31u) / 32u;
118 const size_t expectedSize = 3u + wordsPerLayer * static_cast<size_t>(layers);
119 if (mask.size() < expectedSize) {
120 return styleMaskUnknown;
121 }
122
123 const float u = glm::clamp(quadUV.x, 0.f, 1.f);
124 const float v = glm::clamp(quadUV.y, 0.f, 1.f);
125 const int px = std::min(static_cast<int>(u * static_cast<float>(width)), width - 1);
126 // The quad's v coordinate increases upwards while mask rows are stored top-down, so flip.
127 const int py = std::min(static_cast<int>((1.f - v) * static_cast<float>(height)), height - 1);
128 const size_t p = static_cast<size_t>(py) * static_cast<size_t>(width) + static_cast<size_t>(px);
129 const size_t layerBase = 3u + wordsPerLayer * static_cast<size_t>(layer);
130 const uint32_t word = static_cast<uint32_t>(mask[layerBase + (p >> 5)]);
131 const bool opaque = (word & (1u << (p & 31u))) != 0u;
132 return opaque ? styleMaskOpaque : styleMaskTransparent;
133 }
134
135 void pickScaledFixedScreenSizeBox(RecordHit& recordHit,
136 const BadgeSetComponent& comp,
137 const BadgeSetData& data,
138 const glm::vec3& rayOriginLocal,
139 const glm::vec3& rayUnitDirectionLocal,
140 const glm::mat4& clipFromLocal,
141 const glm::vec2& queryClip,
142 const glm::vec2 viewportSize,
143 const float maxDistanceLocal,
144 const float dpiScale)
145 {
146 const float nearDistance = comp.nearVisibilityLimit;
147 const float farDistance = std::min(comp.farVisibilityLimit, maxDistanceLocal);
148 const float size = comp.badgeSize * dpiScale;
149 const glm::vec4 viewPlaneLocal = data.viewPlaneLocal;
150 const glm::vec3 mainCamViewDirLocal(data.viewPlaneLocal);
151 const glm::vec2 pickPosView = queryClip * viewportSize;
152 const float rcp = -1.f / glm::dot(mainCamViewDirLocal, rayUnitDirectionLocal);
153 const float mp = glm::dot(mainCamViewDirLocal, rayOriginLocal);
154 if (std::isfinite(rcp)) {
155 for (size_t i = 0; i < comp.positions.size(); i++) {
156 const glm::vec4 posLocal(comp.positions[i], 1.f);
157 float viewZ = glm::dot(viewPlaneLocal, posLocal);
158 float scaleFactor = viewZ / glm::clamp(viewZ, comp.nearScalingCutoff, comp.farScalingCutoff);
159 float adjustedPointSize = size * scaleFactor;
160
161 // Adjusting the bounding box so that it fits when/if the anchor of the badge changes
162 glm::vec2 corners[2] = {
163 { comp.boundingBoxLower.x - comp.anchorPoint.x, comp.boundingBoxLower.y - comp.anchorPoint.y},
164 { comp.boundingBoxUpper.x - comp.anchorPoint.x, comp.boundingBoxUpper.y - comp.anchorPoint.y}
165 };
166
167 glm::vec3 cornerBottomLeft = glm::vec3(posLocal) + ((adjustedPointSize * corners[0].x) * data.viewXLocal + (adjustedPointSize * corners[0].y) * data.viewYLocal);
168 glm::vec3 cornerTopRight = glm::vec3(posLocal) + ((adjustedPointSize * corners[1].x) * data.viewXLocal + (adjustedPointSize * corners[1].y) * data.viewYLocal);
169
170 if (nearDistance <= viewZ && viewZ <= farDistance) {
171
172 glm::vec3 cornerBottomLeftClipSpace = euclidean(clipFromLocal * glm::vec4(cornerBottomLeft, 1));
173 glm::vec2 cornerBottomLeftView = glm::vec2(cornerBottomLeftClipSpace) * viewportSize;
174
175 glm::vec3 cornerTopRightClipSpace = euclidean(clipFromLocal * glm::vec4(cornerTopRight, 1));
176 glm::vec2 cornerTopRightView = glm::vec2(cornerTopRightClipSpace) * viewportSize;
177
178 BoundingBox box(cornerBottomLeftView.x, cornerBottomLeftView.y, cornerTopRightView.x, cornerTopRightView.y);
179 const float t = rcp * (mp - glm::dot(glm::vec3(data.viewPlaneLocal), glm::vec3(posLocal)));
180
181 if (box.isPointWithin(pickPosView)) {
182 // Only count the hit if it lands on a non-transparent part of the badge's
183 // style texture. Recover the pick position as a fraction of the projected
184 // bounding box, then remap it into the quad/texture coordinate the shader
185 // samples 'styleMap' with: the bounding box is only a sub-region of the
186 // [0,1] quad, so a non-default boundingBoxLower/Upper must be folded back in
187 // (otherwise the box fraction would be stretched across the whole mask).
188 // When no mask is available sampleStyleMask returns "unknown" and we keep the
189 // geometric hit. anchorPoint cancels out here (it shifts both corners equally).
190 const glm::vec2 boxSpan = cornerTopRightView - cornerBottomLeftView;
191 const glm::vec2 boxFraction = {
192 boxSpan.x != 0.f ? (pickPosView.x - cornerBottomLeftView.x) / boxSpan.x : 0.f,
193 boxSpan.y != 0.f ? (pickPosView.y - cornerBottomLeftView.y) / boxSpan.y : 0.f,
194 };
195 const glm::vec2 texCoord = comp.boundingBoxLower + boxFraction * (comp.boundingBoxUpper - comp.boundingBoxLower);
196 const int maskState = sampleStyleMask(comp, i, texCoord);
197 if (maskState != styleMaskTransparent) {
198 recordHit(posLocal, t, i);
199 }
200 }
201 }
202 }
203 }
204 }
205}
206
207
209 const CameraComponent& camera,
210 const glm::vec2& queryClip,
211 float rayLength,
212 float rayPixelRadius,
213 PickingFlags pickingFlags,
214 PicksReturned returnFlag,
215 const RayPicking::RayPickFilter& filter,
216 std::vector<RayPicking::RayPickHit>& hits)
217{
218 const CameraData& cameraData = context->cameraSystem->getData(&camera);
219 const float dpiScale = context->getDefaultView()->dpiService->getScaleFactor();
220
221 bool hitSomething = false;
222 for (const BadgeSetComponent& comp : system->pool) {
223 if (filter.isUnwantedType(comp)) {
224 continue;
225 }
226
227 const SceneComponent* sceneComp = comp.getComponent<SceneComponent>();
228 if (sceneComp && (!sceneComp->visible || !sceneComp->pickable)) {
229 continue;
230 }
231
232 const RenderComponent* renderComp = comp.getComponent<RenderComponent>();
233 if (renderComp == nullptr || !renderComp->isVisibleInLayer(filter.layerMask)) {
234 continue;
235 }
236
237 const BadgeSetData& data = system->getData(&comp);
238
239 const TransformComponent* transformComp = comp.getComponent<TransformComponent>();
240 const glm::mat4 worldFromLocal = context->transformSystem->getLocalToWorld(transformComp);
241 const glm::mat4 clipFromLocal = cameraData.rawViewProjection * worldFromLocal;
242 const glm::mat4 localFromClip = glm::inverse(clipFromLocal);
243 const glm::vec2 sigma = glm::vec2(2.f * rayPixelRadius) / cameraData.viewportSize;
244 const glm::vec3 rayOriginLocal = euclidean(localFromClip * glm::vec4(queryClip, -1.f, 1.f));
245 const glm::vec3 rayOriginFarLocal = euclidean(localFromClip * glm::vec4(queryClip, 1.f, 1.f));
246 const glm::vec3 rayDirLocal = rayOriginFarLocal - rayOriginLocal;
247 const glm::vec3 rayOriginSigmaLocal = euclidean(localFromClip * glm::vec4(queryClip + sigma, -1.f, 1.f));
248 const glm::vec3 rayOriginSigmaFarLocal = euclidean(localFromClip * glm::vec4(queryClip + sigma, 1.f, 1.f));
249 const float rayLengthRcp = 1.f / glm::length(rayDirLocal);
250 const glm::vec3 rayUnitDirLocal = rayLengthRcp * rayDirLocal;
251 const float worldFromLocalScale = glm::determinant(glm::mat3(worldFromLocal));
252
253 // The min below is to work-around local scale becoming infinity if raylength is max_float.
254 const float maxDistanceLocal = std::min(std::numeric_limits<float>::max(), 2.f * worldFromLocalScale * rayLength);
255
256 RecordHit recordHit{
257 .hits = hits,
258 .comp = comp,
259 .ordinal = filter.getOrdinal(*renderComp),
260 .renderComp = *renderComp,
261 .worldFromLocal = worldFromLocal,
262 .rayFromLocalScale = worldFromLocalScale,
263 .returnClosest = returnFlag == PicksReturned::Closest,
264 .returnChildEntity = (pickingFlags & PickingFlags::ReturnChildEntity) == PickingFlags::ReturnChildEntity
265 };
266
267 pickScaledFixedScreenSizeBox(recordHit, comp, data, rayOriginLocal, rayUnitDirLocal, clipFromLocal, queryClip, cameraData.viewportSize, maxDistanceLocal, dpiScale);
268
269 hitSomething |= recordHit.hitSomething;
270 }
271
272 return hitSomething;
273}
ComponentType * getComponent() const
Definition: Component.h:159
ComponentPool< ComponentType > pool
Pool of components managed by the system.
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
Base component for all rendering content.
constexpr bool isVisibleInLayer(RenderLayers layerMask) const
Check if the entity should be visible in the given layer mask.
Contains information on how the entity behaves in the scene.
bool visible
If the entity this component is a member of should be visible.
bool pickable
If the entity this component is a member of should be pickable.
Defines a 4x4 transformation matrix for the entity and a global offset for root entities.
std::unique_ptr< class DPIService > dpiService
DPI service instance.
Definition: ViewContext.h:71
Log implementation class.
Definition: LogManager.h:140
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
PicksReturned
  * Options for returning picking hits.
Definition: PickingFlags.h:40
@ Closest
Return just the closest hit.
PickingFlags
Options for COGS picking.
Definition: PickingFlags.h:12
@ ReturnChildEntity
Return ID if sub-entity picked, not set: return root parent entity.
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:181
Component for showing lots of badges.
float nearScalingCutoff
Determines the near limit where the badge stops scaling freely.
float farScalingCutoff
Determines the far limit where the badge stops scaling freely.
std::vector< glm::vec3 > positions
Badge positions.
std::vector< int > masks
Packed alpha bit-mask for the style textures, used by picking to reject transparent pixels.
float farVisibilityLimit
Don't display badges further than this distance from the camera.
glm::vec2 boundingBoxLower
Position of the lower corner of the bounding box.
float nearVisibilityLimit
Don't display badges closer than this distance from the camera.
float badgeSize
Size of the badge.
std::vector< int > styles
Style (index) used for background behind icon.
glm::vec2 boundingBoxUpper
Position of the upper corner of the bounding box.
glm::vec2 anchorPoint
The location where the badge is anchored.
bool pickCamera(Context *context, const CameraComponent &camera, const glm::vec2 &queryClip, float, float rayRadius, PickingFlags pickingFlags, PicksReturned returnFlag, const RayPicking::RayPickFilter &filter, std::vector< RayPicking::RayPickHit > &hits) override
Contains data describing a Camera instance and its derived data structured such as matrix data and vi...
Definition: CameraSystem.h:67
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:202
RenderLayers layerMask
Limit picking to the specified render layers. Pick all layers by default.
Definition: RayPick.h:62