Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssgrendershadowmap.cpp
Go to the documentation of this file.
1// Copyright (C) 2008-2012 NVIDIA Corporation.
2// Copyright (C) 2019 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
4
5#include <QtQuick3DRuntimeRender/private/qssgrenderlayer_p.h>
6#include <QtQuick3DRuntimeRender/private/qssgrendershadowmap_p.h>
7#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
9
11
12namespace AtlasHelpers {
13
15 struct Shelf {
16 int y;
17 int height;
18 int curX;
19 };
20
21 struct ShelfPage {
23 int width;
24 int height;
25 int curY;
27 };
28
30 bool success;
32 int x;
33 int y;
34 };
35 const int pageWidth;
36 const int pageHeight;
38
39 ShelfPacker(int pageWidth, int pageHeight)
40 : pageWidth(pageWidth)
41 , pageHeight(pageHeight)
42 {
43 }
44
45 AtlasPlacement addRectangle(int sizeNeeded) {
46 // Try each page in turn
47 for (auto &page : pages) {
48 AtlasPlacement placement = placeOnPage(page, sizeNeeded);
49 if (placement.success)
50 return placement;
51 }
52
53 // If we get here, we need a new page
54 ShelfPage newPage;
55 newPage.pageIndex = int(pages.size());
56 newPage.width = pageWidth;
57 newPage.height = pageHeight;
58 newPage.curY = 0;
59 newPage.shelves.clear();
60 pages.push_back(newPage);
61
62 // now place in the new page
63 return placeOnPage(pages.back(), sizeNeeded);
64 }
65
66 AtlasPlacement placeOnPage(ShelfPage &page, int sizeNeeded)
67 {
68 AtlasPlacement result;
69 result.success = false;
70
71 // Iterate over shelves to see if we can place the rect
72 for (auto &shelf : page.shelves) {
73 // check if it fits horizontally
74 if (shelf.curX + sizeNeeded <= page.width) {
75 // also check if the shelf's height is enough
76 // but here we store the shelf's "height" as the max of the rectangles' heights
77 // If each rect is the same "sizeNeeded," we just need to see if there's room in page.height
78
79 // not enough vertical space
80 if (shelf.y + shelf.height >= page.height)
81 continue;
82
83 // place here
84 result.success = true;
85 result.pageIndex = page.pageIndex;
86 result.x = shelf.curX;
87 result.y = shelf.y;
88 // update shelf
89 shelf.curX += sizeNeeded;
90 if (sizeNeeded > shelf.height)
91 shelf.height = sizeNeeded;
92
93 return result;
94 }
95 }
96
97 // No existing shelf had space, so start a new shelf
98 int newShelfY = 0;
99 if (!page.shelves.empty()) {
100 const Shelf &lastShelf = page.shelves.back();
101 newShelfY = lastShelf.y + lastShelf.height;
102 }
103
104 // check if there's enough space left in the page vertically
105 if (newShelfY + sizeNeeded > page.height) {
106 // no space in this page
107 return result; // success = false
108 }
109
110 // create a new shelf
111 Shelf newShelf;
112 newShelf.y = newShelfY;
113 newShelf.height = sizeNeeded;
114 newShelf.curX = 0;
115 page.shelves.push_back(newShelf);
116
117 // place rect at top-left of this shelf
118 Shelf &shelfRef = page.shelves.back();
119 result.success = true;
120 result.pageIndex = page.pageIndex;
121 result.x = shelfRef.curX;
122 result.y = shelfRef.y;
123 shelfRef.curX += sizeNeeded;
124
125 return result;
126 }
127
128 int pagesNeeded() const { return int(pages.size()); }
129};
130
131}
132
133static QRhiTexture *allocateRhiShadowTexture(QRhi *rhi, QRhiTexture::Format format, const QSize &size, quint32 numLayers, QRhiTexture::Flags flags)
134{
135 auto texture = rhi->newTexture(format, size, 1, flags);
136 if (flags & QRhiTexture::TextureArray)
137 texture->setArraySize(numLayers);
138 if (!texture->create())
139 qWarning("Failed to create shadow map texture of size %dx%d", size.width(), size.height());
140 return texture;
141}
142
143static QRhiRenderBuffer *allocateRhiShadowRenderBuffer(QRhi *rhi, QRhiRenderBuffer::Type type, const QSize &size)
144{
145 auto renderBuffer = rhi->newRenderBuffer(type, size, 1);
146 if (!renderBuffer->create())
147 qWarning("Failed to build depth-stencil buffer of size %dx%d", size.width(), size.height());
148 return renderBuffer;
149}
150
151
152static bool checkCompatibility(QSSGShadowMapEntry *entry, const QVector<QSSGShadowMapEntry::AtlasEntry> &atlasEntries) {
153 if (!entry)
154 return false;
155
156 for (int i = 0; i < atlasEntries.size(); ++i) {
157 const auto &atlasEntry = atlasEntries.at(i);
158
159 if (!(entry->m_atlasInfo[i].layerIndex == atlasEntry.layerIndex &&
160 qFuzzyCompare(entry->m_atlasInfo[i].uOffset, atlasEntry.uOffset) &&
161 qFuzzyCompare(entry->m_atlasInfo[i].vOffset, atlasEntry.vOffset) &&
162 qFuzzyCompare(entry->m_atlasInfo[i].uvScale, atlasEntry.uvScale)))
163 return false;
164 }
165
166 return true;
167}
168
169static QVector<QSSGShadowMapEntry::AtlasEntry> createAtlasEntries(const QVarLengthArray<AtlasHelpers::ShelfPacker::AtlasPlacement, 4> &atlasPlacements, int entrySize, int atlasPageSize) {
170 QVector<QSSGShadowMapEntry::AtlasEntry> atlasEntries;
171 atlasEntries.reserve(atlasPlacements.size());
172 for (int i = 0; i < atlasPlacements.size(); ++i) {
173 const auto &placement = atlasPlacements.at(i);
174 const float x = float(placement.x) / float(atlasPageSize);
175 const float y = float(placement.y) / float(atlasPageSize);
176 const float uvScale = float(entrySize) / float(atlasPageSize);
177 atlasEntries.append({placement.pageIndex, x, y, uvScale});
178 }
179 return atlasEntries;
180}
181
182
183QSSGRenderShadowMap::QSSGRenderShadowMap(const QSSGRenderContextInterface &inContext)
184 : m_context(inContext)
185{
186}
187
188QSSGRenderShadowMap::~QSSGRenderShadowMap()
189{
190 releaseCachedResources();
191}
192
193void QSSGRenderShadowMap::releaseCachedResources()
194{
195 for (QSSGShadowMapEntry &entry : m_shadowMapList)
196 entry.destroyRhiResources();
197
198 if (m_shadowMapAtlasTexture)
199 m_shadowMapAtlasTexture.reset();
200
201 qDeleteAll(m_layerDepthStencilBuffers);
202 m_layerDepthStencilBuffers.clear();
203 qDeleteAll(m_layerRenderTargets);
204 m_layerRenderTargets.clear();
205 qDeleteAll(m_layerRenderPassDescriptors);
206 m_layerRenderPassDescriptors.clear();
207
208 m_shadowMapList.clear();
209}
210
211void QSSGRenderShadowMap::addShadowMaps(const QSSGShaderLightList &renderableLights)
212{
213 QRhi *rhi = m_context.rhiContext()->rhi();
214 // Bail out if there is no QRhi, since we can't add entries without it
215 if (!rhi)
216 return;
217
218 const quint32 numLights = renderableLights.size();
219 qsizetype numShadows = 0;
220 const bool supports32BitTextures = rhi->isTextureFormatSupported(QRhiTexture::R32F);
221 QRhiTexture::Format format = QRhiTexture::R16F;
222 quint32 mapSize = 0;
223 QHash<quint32, QVector<QSSGShadowMapEntry::AtlasEntry>> lightIndexToAtlasEntries;
224
225 // Get format and maximum texture size needed
226 for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
227 const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
228 if (!shaderLight.shadows)
229 continue;
230
231 // Force R32F format if any light requests it
232 if (shaderLight.light->m_use32BitShadowmap && supports32BitTextures)
233 format = QRhiTexture::R32F;
234
235 // Find the largest shadow map size needed
236 if (mapSize < shaderLight.light->m_shadowMapRes)
237 mapSize = shaderLight.light->m_shadowMapRes;
238
239 numShadows += 1;
240 }
241
242 auto atlasPacker = AtlasHelpers::ShelfPacker(mapSize, mapSize);
243
244
245 // Figure out the number of layers needed for the atlas
246 // including where everthing will go in the atlas
247 for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
248 const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
249 if (!shaderLight.shadows)
250 continue;
251
252 quint8 mapsNeeded = 0;
253
254 if (shaderLight.light->type == QSSGRenderLight::Type::DirectionalLight)
255 mapsNeeded = shaderLight.light->m_csmNumSplits + 1;
256 else if (shaderLight.light->type == QSSGRenderLight::Type::SpotLight)
257 mapsNeeded = 1;
258 else if (shaderLight.light->type == QSSGRenderLight::Type::PointLight)
259 mapsNeeded = 2;
260
261 QVarLengthArray<AtlasHelpers::ShelfPacker::AtlasPlacement, 4> atlasPlacements;
262 for (quint8 i = 0; i < mapsNeeded; ++i) {
263 const int size = shaderLight.light->m_shadowMapRes;
264 auto result = atlasPacker.addRectangle(size);
265 if (result.success)
266 atlasPlacements.push_back(result);
267 }
268 lightIndexToAtlasEntries.insert(lightIndex, createAtlasEntries(atlasPlacements, shaderLight.light->m_shadowMapRes, mapSize));
269 }
270 const QSize texSize = QSize(mapSize, mapSize);
271 const quint32 layersNeeded = atlasPacker.pagesNeeded();
272
273 // Check if we need a rebuild
274 bool needsRebuild = numShadows != shadowMapEntryCount();
275 if (!m_shadowMapAtlasTexture ||
276 m_shadowMapAtlasTexture->pixelSize() != texSize ||
277 m_shadowMapAtlasTexture->arraySize() != int(layersNeeded) ||
278 m_shadowMapAtlasTexture->format() != format) {
279
280 // If we need a new texture as well
281 if (m_shadowMapAtlasTexture) {
282 m_shadowMapAtlasTexture.reset();
283 }
284
285 m_shadowMapAtlasTexture.reset(allocateRhiShadowTexture(rhi, format, texSize, layersNeeded, QRhiTexture::RenderTarget | QRhiTexture::TextureArray));
286
287 qDeleteAll(m_layerDepthStencilBuffers);
288 m_layerDepthStencilBuffers.clear();
289 qDeleteAll(m_layerRenderTargets);
290 m_layerRenderTargets.clear();
291 qDeleteAll(m_layerRenderPassDescriptors);
292 m_layerRenderPassDescriptors.clear();
293
294 for (quint32 i = 0; i < layersNeeded; ++i) {
295 // Recreate per layer RenderBuffers, TextureRenderTarget, and RenderPassDescriptors
296 QRhiRenderBuffer *depthStencilBuffer = allocateRhiShadowRenderBuffer(rhi, QRhiRenderBuffer::DepthStencil, texSize);
297 QRhiTextureRenderTargetDescription rtDesc;
298 QRhiColorAttachment attachment(m_shadowMapAtlasTexture.get());
299 attachment.setLayer(i);
300 rtDesc.setColorAttachments({ attachment });
301 rtDesc.setDepthStencilBuffer(depthStencilBuffer);
302 QRhiTextureRenderTarget *rt = rhi->newTextureRenderTarget(rtDesc);
303 rt->setDescription(rtDesc);
304 rt->setFlags(QRhiTextureRenderTarget::PreserveColorContents); // Don't clear between passes since this is an atlas
305 QRhiRenderPassDescriptor *rpDesc = rt->newCompatibleRenderPassDescriptor();
306 rt->setRenderPassDescriptor(rpDesc);
307 if (!rt->create())
308 qWarning("Failed to build shadow map render target");
309 rt->setName(QByteArrayLiteral("shadow map atlas layer") + QByteArray::number(i));
310 m_layerDepthStencilBuffers.append(depthStencilBuffer);
311 m_layerRenderTargets.append(rt);
312 m_layerRenderPassDescriptors.append(rpDesc);
313 }
314
315 needsRebuild = true;
316 }
317
318 // If we need to allocate the RHI resources
319 if (!m_sharedFrontCubeToAtlasUniformBuffer || !m_sharedBackCubeToAtlasUniformBuffer) {
320 const quint32 uniformValueFront = 0;
321 const quint32 uniformValueBack = 1;
322
323 QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
324 if (!m_sharedFrontCubeToAtlasUniformBuffer) {
325 m_sharedFrontCubeToAtlasUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(uint32_t)));
326 m_sharedFrontCubeToAtlasUniformBuffer->create();
327 rub->updateDynamicBuffer(m_sharedFrontCubeToAtlasUniformBuffer.get(), 0, sizeof(quint32), &uniformValueFront);
328 }
329
330 if (!m_sharedBackCubeToAtlasUniformBuffer) {
331 m_sharedBackCubeToAtlasUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(uint32_t)));
332 m_sharedBackCubeToAtlasUniformBuffer->create();
333 rub->updateDynamicBuffer(m_sharedBackCubeToAtlasUniformBuffer.get(), 0, sizeof(quint32), &uniformValueBack);
334 }
335 QRhiCommandBuffer *cb = m_context.rhiContext()->commandBuffer();
336 cb->resourceUpdate(rub);
337 }
338 if (!m_sharedCubeToAtlasSampler) {
339 m_sharedCubeToAtlasSampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
340 m_sharedCubeToAtlasSampler->create();
341 }
342 if (!m_shadowClearSrb) {
343 m_shadowClearSrb.reset(rhi->newShaderResourceBindings());
344 m_shadowClearSrb->create();
345 }
346
347
348 if (!needsRebuild) {
349 // Check if relevant shadow properties has changed
350 for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
351 const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
352 if (!shaderLight.shadows)
353 continue;
354 QSSGShadowMapEntry *pEntry = shadowMapEntry(lightIndex);
355 if (!pEntry) {
356 needsRebuild = true;
357 break;
358 }
359
360 const auto &atlasEntires = lightIndexToAtlasEntries.value(lightIndex);
361 if (!checkCompatibility(pEntry, atlasEntires)) {
362 needsRebuild = true;
363 break;
364 }
365 }
366 }
367
368 if (!needsRebuild)
369 return;
370
371 // Rebuild then
372 for (QSSGShadowMapEntry &entry : m_shadowMapList)
373 entry.destroyRhiResources();
374 m_shadowMapList.clear();
375
376 for (quint32 lightIndex = 0; lightIndex < numLights; ++lightIndex) {
377 const QSSGShaderLight &shaderLight = renderableLights.at(lightIndex);
378 if (!shaderLight.shadows)
379 continue;
380 addShadowMap(lightIndex,
381 texSize,
382 lightIndexToAtlasEntries.value(lightIndex),
383 shaderLight.light->m_csmNumSplits,
384 format,
385 shaderLight.light->type == QSSGRenderLight::Type::PointLight,
386 shaderLight.light->debugObjectName);
387 }
388
389}
390
391QSSGShadowMapEntry *QSSGRenderShadowMap::addShadowMap(quint32 lightIdx,
392 QSize size,
393 QVector<QSSGShadowMapEntry::AtlasEntry> atlasEntries,
394 quint32 csmNumSplits,
395 QRhiTexture::Format rhiFormat,
396 bool isPointLight,
397 const QString &renderNodeObjName)
398{
399 QRhi *rhi = m_context.rhiContext()->rhi();
400 QSSGShadowMapEntry *pEntry = shadowMapEntry(lightIdx);
401
402 Q_ASSERT(rhi);
403 Q_ASSERT(!pEntry);
404 Q_ASSERT(!atlasEntries.isEmpty());
405
406 m_shadowMapList.push_back(QSSGShadowMapEntry::withAtlas(lightIdx));
407
408 pEntry = &m_shadowMapList.back();
409 pEntry->m_csmNumSplits = csmNumSplits;
410
411 if (isPointLight) {
412 const QSize localSize = size * atlasEntries.first().uvScale;
413 // First pass renders to depth cube map
414 const QByteArray rtName = renderNodeObjName.toLatin1();
415
416 pEntry->m_rhiDepthCube = allocateRhiShadowTexture(rhi, rhiFormat, localSize, 0, QRhiTexture::RenderTarget | QRhiTexture::CubeMap);
417 pEntry->m_rhiDepthStencilCube = allocateRhiShadowRenderBuffer(rhi, QRhiRenderBuffer::DepthStencil, localSize);
418
419
420 for (const auto face : QSSGRenderTextureCubeFaces) {
421 QRhiTextureRenderTarget *&rt(pEntry->m_rhiRenderTargetCube[quint8(face)]);
422 Q_ASSERT(!rt);
423 QRhiColorAttachment att(pEntry->m_rhiDepthCube);
424 att.setLayer(quint8(face)); // 6 render targets, each referencing one face of the cubemap
425 QRhiTextureRenderTargetDescription rtDesc;
426 rtDesc.setColorAttachments({ att });
427 rtDesc.setDepthStencilBuffer(pEntry->m_rhiDepthStencilCube);
428 rt = rhi->newTextureRenderTarget(rtDesc);
429 rt->setDescription(rtDesc);
430 if (!pEntry->m_rhiRenderPassDescCube)
431 pEntry->m_rhiRenderPassDescCube = rt->newCompatibleRenderPassDescriptor();
432 rt->setRenderPassDescriptor(pEntry->m_rhiRenderPassDescCube);
433 if (!rt->create())
434 qWarning("Failed to build shadow map render target");
435 rt->setName(rtName + QByteArrayLiteral(" shadow cube face: ") + QSSGBaseTypeHelpers::displayName(face));
436 }
437
438 if (!pEntry->m_cubeToAtlasFrontSrb) {
439 pEntry->m_cubeToAtlasFrontSrb = rhi->newShaderResourceBindings();
440 pEntry->m_cubeToAtlasFrontSrb->setBindings({
441 QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, m_sharedFrontCubeToAtlasUniformBuffer.get()),
442 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, pEntry->m_rhiDepthCube, m_sharedCubeToAtlasSampler.get())
443 });
444 pEntry->m_cubeToAtlasFrontSrb->create();
445 }
446
447 if (!pEntry->m_cubeToAtlasBackSrb) {
448 pEntry->m_cubeToAtlasBackSrb = rhi->newShaderResourceBindings();
449 pEntry->m_cubeToAtlasBackSrb->setBindings({
450 QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, m_sharedBackCubeToAtlasUniformBuffer.get()),
451 QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, pEntry->m_rhiDepthCube, m_sharedCubeToAtlasSampler.get())
452 });
453 pEntry->m_cubeToAtlasBackSrb->create();
454 }
455 }
456
457
458 // Additional graphics resources: samplers, render targets.
459 const quint32 entriesCount = atlasEntries.size();
460 for (quint32 splitIndex = 0; splitIndex < entriesCount; splitIndex++) {
461 pEntry->m_atlasInfo[splitIndex].layerIndex = atlasEntries.at(splitIndex).layerIndex;
462 pEntry->m_atlasInfo[splitIndex].uOffset = atlasEntries.at(splitIndex).uOffset;
463 pEntry->m_atlasInfo[splitIndex].vOffset = atlasEntries.at(splitIndex).vOffset;
464 pEntry->m_atlasInfo[splitIndex].uvScale = atlasEntries.at(splitIndex).uvScale;
465
466 quint32 layerId = atlasEntries.at(splitIndex).layerIndex;
467 pEntry->m_rhiRenderTargets[splitIndex] = m_layerRenderTargets[layerId];
468 pEntry->m_rhiRenderPassDesc[splitIndex] = m_layerRenderPassDescriptors[layerId];
469 }
470 pEntry->m_lightIndex = lightIdx;
471
472 return pEntry;
473}
474
475QSSGShadowMapEntry *QSSGRenderShadowMap::shadowMapEntry(int lightIdx)
476{
477 Q_ASSERT(lightIdx >= 0);
478
479 for (int i = 0; i < m_shadowMapList.size(); i++) {
480 QSSGShadowMapEntry *pEntry = &m_shadowMapList[i];
481 if (pEntry->m_lightIndex == quint32(lightIdx))
482 return pEntry;
483 }
484
485 return nullptr;
486}
487
488QRhiTextureRenderTarget *QSSGRenderShadowMap::layerRenderTarget(int layerIndex)
489{
490 if (layerIndex < 0 || layerIndex >= m_layerRenderTargets.size())
491 return nullptr;
492 return m_layerRenderTargets.at(layerIndex);
493}
494
495QRhiRenderPassDescriptor *QSSGRenderShadowMap::layerRenderPassDescriptor(int layerIndex)
496{
497 if (layerIndex < 0 || layerIndex >= m_layerRenderPassDescriptors.size())
498 return nullptr;
499 return m_layerRenderPassDescriptors.at(layerIndex);
500}
501
502QRhiTexture *QSSGRenderShadowMap::shadowMapAtlasTexture() const
503{
504 return m_shadowMapAtlasTexture.get();
505}
506
507QSSGShadowMapEntry::QSSGShadowMapEntry()
508 : m_lightIndex(std::numeric_limits<quint32>::max())
509{
510}
511
512QSSGShadowMapEntry QSSGShadowMapEntry::withAtlas(quint32 lightIdx)
513{
514 QSSGShadowMapEntry e;
515 e.m_lightIndex = lightIdx;
516 return e;
517}
518
519void QSSGShadowMapEntry::destroyRhiResources()
520{
521 delete m_rhiDepthCube;
522 m_rhiDepthCube = nullptr;
523 delete m_rhiDepthStencilCube;
524 m_rhiDepthStencilCube = nullptr;
525 qDeleteAll(m_rhiRenderTargetCube);
526 m_rhiRenderTargetCube.fill(nullptr);
527 delete m_rhiRenderPassDescCube;
528 m_rhiRenderPassDescCube = nullptr;
529 delete m_cubeToAtlasFrontSrb;
530 m_cubeToAtlasFrontSrb = nullptr;
531 delete m_cubeToAtlasBackSrb;
532 m_cubeToAtlasBackSrb = nullptr;
533
534 // un-owned references
535 m_rhiRenderTargets.fill(nullptr);
536 m_rhiRenderPassDesc.fill(nullptr);
537}
538
539QT_END_NAMESPACE
static QVector< QSSGShadowMapEntry::AtlasEntry > createAtlasEntries(const QVarLengthArray< AtlasHelpers::ShelfPacker::AtlasPlacement, 4 > &atlasPlacements, int entrySize, int atlasPageSize)
static bool checkCompatibility(QSSGShadowMapEntry *entry, const QVector< QSSGShadowMapEntry::AtlasEntry > &atlasEntries)
static QRhiTexture * allocateRhiShadowTexture(QRhi *rhi, QRhiTexture::Format format, const QSize &size, quint32 numLayers, QRhiTexture::Flags flags)
static QRhiRenderBuffer * allocateRhiShadowRenderBuffer(QRhi *rhi, QRhiRenderBuffer::Type type, const QSize &size)
AtlasPlacement addRectangle(int sizeNeeded)
AtlasPlacement placeOnPage(ShelfPage &page, int sizeNeeded)
std::vector< ShelfPage > pages
ShelfPacker(int pageWidth, int pageHeight)