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/qssgrenderloadedtexture_p.h>
6#include <QtQuick3DRuntimeRender/private/qssgrendererutil_p.h>
7#include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
8#include <QtQuick3DRuntimeRender/private/qssgrendertexturedata_p.h>
9#include <QtGui/QImageReader>
10#include <QtGui/QColorSpace>
11#include <QtMath>
12
13#include <QtQuick3DUtils/private/qssgutils_p.h>
14
15#include <private/qtexturefilereader_p.h>
16
17#define TINYEXR_IMPLEMENTATION
18#define TINYEXR_USE_MINIZ 0
19#define TINYEXR_USE_THREAD 1
20#include <zlib.h>
21#include <tinyexr.h>
22
23QT_BEGIN_NAMESPACE
24
25
26QSharedPointer<QIODevice> QSSGInputUtil::getStreamForFile(const QString &inPath, bool inQuiet, QString *outPath)
27{
28 QFile *file = nullptr;
29 QString tryPath = inPath.startsWith(s: QLatin1String("qrc:/")) ? inPath.mid(position: 3) : inPath;
30 QFileInfo fi(tryPath);
31 bool found = fi.exists();
32 if (!found && fi.isNativePath()) {
33 tryPath.prepend(s: QLatin1String(":/"));
34 fi.setFile(tryPath);
35 found = fi.exists();
36 }
37 if (found) {
38 QString filePath = fi.canonicalFilePath();
39 file = new QFile(filePath);
40 if (file->open(flags: QIODevice::ReadOnly)) {
41 if (outPath)
42 *outPath = filePath;
43 } else {
44 delete file;
45 file = nullptr;
46 }
47 }
48 if (!file && !inQuiet)
49 qCWarning(WARNING, "Failed to find file: %s", qPrintable(inPath));
50 return QSharedPointer<QIODevice>(file);
51}
52
53QSharedPointer<QIODevice> QSSGInputUtil::getStreamForTextureFile(const QString &inPath, bool inQuiet,
54 QString *outPath, FileType *outFileType)
55{
56 static const QList<QByteArray> hdrFormats = QList<QByteArray>({ "hdr", "exr" });
57 static const QList<QByteArray> textureFormats = QTextureFileReader::supportedFileFormats();
58 static const QList<QByteArray> imageFormats = QImageReader::supportedImageFormats();
59 static const QList<QByteArray> allFormats = textureFormats + hdrFormats + imageFormats;
60
61 QString filePath;
62 QByteArray ext;
63 QSharedPointer<QIODevice> stream = getStreamForFile(inPath, inQuiet: true, outPath: &filePath);
64 if (stream) {
65 ext = QFileInfo(filePath).suffix().toLatin1().toLower();
66 } else {
67 for (const QByteArray &format : allFormats) {
68 QString tryName = inPath + QLatin1Char('.') + QLatin1String(format);
69 stream = getStreamForFile(inPath: tryName, inQuiet: true, outPath: &filePath);
70 if (stream) {
71 ext = format;
72 break;
73 }
74 }
75 }
76 if (stream) {
77 if (outPath)
78 *outPath = filePath;
79 if (outFileType) {
80 FileType type = UnknownFile;
81 if (hdrFormats.contains(t: ext))
82 type = HdrFile;
83 else if (textureFormats.contains(t: ext))
84 type = TextureFile;
85 else if (imageFormats.contains(t: ext))
86 type = ImageFile;
87 *outFileType = type;
88 }
89 } else if (!inQuiet) {
90 qCWarning(WARNING, "Failed to find texture file for: %s", qPrintable(inPath));
91 }
92 return stream;
93}
94
95static inline QSSGRenderTextureFormat fromGLtoTextureFormat(quint32 internalFormat)
96{
97 switch (internalFormat) {
98 case 0x8229:
99 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R8);
100 case 0x822A:
101 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R16);
102 case 0x822D:
103 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R16F);
104 case 0x8235:
105 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32I);
106 case 0x8236:
107 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32UI);
108 case 0x822E:
109 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32F);
110 case 0x822B:
111 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG8);
112 case 0x8058:
113 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8);
114 case 0x8051:
115 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8);
116 case 0x8C41:
117 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8);
118 case 0x8C43:
119 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8A8);
120 case 0x8D62:
121 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB565);
122 case 0x803C:
123 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Alpha8);
124 case 0x8040:
125 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Luminance8);
126 case 0x8042:
127 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Luminance16);
128 case 0x8045:
129 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::LuminanceAlpha8);
130 case 0x881A:
131 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16F);
132 case 0x822F:
133 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG16F);
134 case 0x8230:
135 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG32F);
136 case 0x8815:
137 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32F);
138 case 0x8814:
139 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32F);
140 case 0x8C3A:
141 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11G11B10);
142 case 0x8C3D:
143 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB9E5);
144 case 0x8059:
145 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB10_A2);
146 case 0x881B:
147 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16F);
148 case 0x8D70:
149 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32UI);
150 case 0x8D71:
151 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32UI);
152 case 0x8D76:
153 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16UI);
154 case 0x8D77:
155 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16UI);
156 case 0x8D7C:
157 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8UI);
158 case 0x8D7D:
159 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8UI);
160 case 0x8D82:
161 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32I);
162 case 0x8D83:
163 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32I);
164 case 0x8D88:
165 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16I);
166 case 0x8D89:
167 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16I);
168 case 0x8D8E:
169 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8I);
170 case 0x8D8F:
171 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8I);
172 case 0x83F1:
173 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT1);
174 case 0x83F0:
175 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB_DXT1);
176 case 0x83F2:
177 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT3);
178 case 0x83F3:
179 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT5);
180 case 0x9270:
181 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11_EAC_UNorm);
182 case 0x9271:
183 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11_EAC_SNorm);
184 case 0x9272:
185 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG11_EAC_UNorm);
186 case 0x9273:
187 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG11_EAC_SNorm);
188 case 0x9274:
189 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8_ETC2);
190 case 0x9275:
191 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_ETC2);
192 case 0x9276:
193 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8_PunchThrough_Alpha1_ETC2);
194 case 0x9277:
195 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_PunchThrough_Alpha1_ETC2);
196 case 0x9278:
197 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8_ETC2_EAC);
198 case 0x9279:
199 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ETC2_EAC);
200 case 0x93B0:
201 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_4x4);
202 case 0x93B1:
203 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_5x4);
204 case 0x93B2:
205 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_5x5);
206 case 0x93B3:
207 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_6x5);
208 case 0x93B4:
209 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_6x6);
210 case 0x93B5:
211 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x5);
212 case 0x93B6:
213 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x6);
214 case 0x93B7:
215 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x8);
216 case 0x93B8:
217 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x5);
218 case 0x93B9:
219 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x6);
220 case 0x93BA:
221 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x8);
222 case 0x93BB:
223 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x10);
224 case 0x93BC:
225 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_12x10);
226 case 0x93BD:
227 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_12x12);
228 case 0x93D0:
229 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_4x4);
230 case 0x93D1:
231 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x4);
232 case 0x93D2:
233 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x5);
234 case 0x93D3:
235 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x5);
236 case 0x93D4:
237 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x6);
238 case 0x93D5:
239 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x5);
240 case 0x93D6:
241 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x6);
242 case 0x93D7:
243 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x8);
244 case 0x93D8:
245 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x5);
246 case 0x93D9:
247 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x6);
248 case 0x93DA:
249 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x8);
250 case 0x93DB:
251 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x10);
252 case 0x93DC:
253 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x10);
254 case 0x93DD:
255 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x12);
256 case 0x81A5:
257 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth16);
258 case 0x81A6:
259 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth24);
260 case 0x81A7:
261 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth32);
262 case 0x88F0:
263 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth24Stencil8);
264 default:
265 return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Unknown);
266 }
267}
268
269static QImage loadImage(const QString &inPath, bool flipVertical)
270{
271 QImage image(inPath);
272 if (image.isNull())
273 return image;
274 const QPixelFormat pixFormat = image.pixelFormat();
275 QImage::Format targetFormat = QImage::Format_RGBA8888_Premultiplied;
276 if (image.colorCount()) // a palleted image
277 targetFormat = QImage::Format_RGBA8888;
278 else if (pixFormat.channelCount() == 1)
279 targetFormat = QImage::Format_Grayscale8;
280 else if (pixFormat.alphaUsage() == QPixelFormat::IgnoresAlpha)
281 targetFormat = QImage::Format_RGBX8888;
282 else if (pixFormat.premultiplied() == QPixelFormat::NotPremultiplied)
283 targetFormat = QImage::Format_RGBA8888;
284
285 image.convertTo(f: targetFormat); // convert to a format mappable to QRhiTexture::Format
286 if (flipVertical)
287 image.mirror(); // Flip vertically to the conventional Y-up orientation
288 return image;
289}
290
291QSSGLoadedTexture *QSSGLoadedTexture::loadQImage(const QString &inPath, qint32 flipVertical)
292{
293 QImage image = loadImage(inPath, flipVertical);
294 if (image.isNull())
295 return nullptr;
296 QSSGLoadedTexture *retval = new QSSGLoadedTexture;
297 retval->width = image.width();
298 retval->height = image.height();
299 retval->components = image.pixelFormat().channelCount();
300 retval->image = image;
301 retval->data = (void *)retval->image.bits();
302 retval->dataSizeInBytes = image.sizeInBytes();
303 retval->setFormatFromComponents();
304 // #TODO: This is a very crude way detect color space
305 retval->isSRGB = image.colorSpace().transferFunction() != QColorSpace::TransferFunction::Linear;
306
307 return retval;
308}
309
310QSSGLoadedTexture *QSSGLoadedTexture::loadCompressedImage(const QString &inPath)
311{
312 QSSGLoadedTexture *retval = nullptr;
313
314 // Open File
315 QFile imageFile(inPath);
316 if (!imageFile.open(flags: QIODevice::ReadOnly)) {
317 qWarning() << "Could not open image file: " << inPath;
318 return retval;
319 }
320 auto reader = new QTextureFileReader(&imageFile, inPath);
321
322 if (!reader->canRead()) {
323 qWarning() << "Unable to read image file: " << inPath;
324 delete reader;
325 return retval;
326 }
327 retval = new QSSGLoadedTexture;
328 retval->textureFileData = reader->read();
329
330 // Fill out what makes sense, leave the rest at the default 0 and null.
331 retval->width = retval->textureFileData.size().width();
332 retval->height = retval->textureFileData.size().height();
333 auto glFormat = retval->textureFileData.glInternalFormat()
334 ? retval->textureFileData.glInternalFormat()
335 : retval->textureFileData.glFormat();
336 retval->format = fromGLtoTextureFormat(internalFormat: glFormat);
337
338 delete reader;
339 imageFile.close();
340
341 return retval;
342
343}
344
345namespace {
346typedef unsigned char RGBE[4];
347#define R 0
348#define G 1
349#define B 2
350#define E 3
351
352#define MINELEN 8 // minimum scanline length for encoding
353#define MAXELEN 0x7fff // maximum scanline length for encoding
354
355
356
357inline int calculateLine(int width, int bitdepth) { return ((width * bitdepth) + 7) / 8; }
358
359inline int calculatePitch(int line) { return (line + 3) & ~3; }
360
361float convertComponent(int exponent, int val)
362{
363 float v = val / (256.0f);
364 float d = powf(x: 2.0f, y: (float)exponent - 128.0f);
365 return v * d;
366}
367
368void decrunchScanline(const char *&p, const char *pEnd, RGBE *scanline, int w)
369{
370 scanline[0][R] = *p++;
371 scanline[0][G] = *p++;
372 scanline[0][B] = *p++;
373 scanline[0][E] = *p++;
374
375 if (scanline[0][R] == 2 && scanline[0][G] == 2 && scanline[0][B] < 128) {
376 // new rle, the first pixel was a dummy
377 for (int channel = 0; channel < 4; ++channel) {
378 for (int x = 0; x < w && p < pEnd; ) {
379 unsigned char c = *p++;
380 if (c > 128) { // run
381 if (p < pEnd) {
382 int repCount = c & 127;
383 c = *p++;
384 while (repCount--)
385 scanline[x++][channel] = c;
386 }
387 } else { // not a run
388 while (c-- && p < pEnd)
389 scanline[x++][channel] = *p++;
390 }
391 }
392 }
393 } else {
394 // old rle
395 scanline[0][R] = 2;
396 int bitshift = 0;
397 int x = 1;
398 while (x < w && pEnd - p >= 4) {
399 scanline[x][R] = *p++;
400 scanline[x][G] = *p++;
401 scanline[x][B] = *p++;
402 scanline[x][E] = *p++;
403
404 if (scanline[x][R] == 1 && scanline[x][G] == 1 && scanline[x][B] == 1) { // run
405 int repCount = scanline[x][3] << bitshift;
406 while (repCount--) {
407 memcpy(dest: scanline[x], src: scanline[x - 1], n: 4);
408 ++x;
409 }
410 bitshift += 8;
411 } else { // not a run
412 ++x;
413 bitshift = 0;
414 }
415 }
416 }
417}
418
419void decodeScanlineToTexture(RGBE *scanline, int width, void *outBuf, quint32 offset, QSSGRenderTextureFormat inFormat)
420{
421 quint8 *target = reinterpret_cast<quint8 *>(outBuf);
422 target += offset;
423
424 if (inFormat == QSSGRenderTextureFormat::RGBE8) {
425 memcpy(dest: target, src: scanline, n: size_t(width) * 4);
426 } else {
427 float rgbaF32[4];
428 for (int i = 0; i < width; ++i) {
429 rgbaF32[R] = convertComponent(exponent: scanline[i][E], val: scanline[i][R]);
430 rgbaF32[G] = convertComponent(exponent: scanline[i][E], val: scanline[i][G]);
431 rgbaF32[B] = convertComponent(exponent: scanline[i][E], val: scanline[i][B]);
432 rgbaF32[3] = 1.0f;
433
434 inFormat.encodeToPixel(inPtr: rgbaF32, outPtr: target, byteOfs: i * inFormat.getSizeofFormat());
435 }
436 }
437}
438
439QSSGLoadedTexture *loadRadianceHdr(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat &format)
440{
441 QSSGLoadedTexture *imageData = nullptr;
442
443 char sig[256];
444 source->seek(pos: 0);
445 source->read(data: sig, maxlen: 11);
446 if (!strncmp(s1: sig, s2: "#?RADIANCE\n", n: 11)) {
447 QByteArray buf = source->readAll();
448 const char *p = buf.constData();
449 const char *pEnd = p + buf.size();
450
451 // Process lines until the empty one.
452 QByteArray line;
453 while (p < pEnd) {
454 char c = *p++;
455 if (c == '\n') {
456 if (line.isEmpty())
457 break;
458 if (line.startsWith(QByteArrayLiteral("FORMAT="))) {
459 const QByteArray format = line.mid(index: 7).trimmed();
460 if (format != QByteArrayLiteral("32-bit_rle_rgbe")) {
461 qWarning(msg: "HDR format '%s' is not supported", format.constData());
462 return imageData;
463 }
464 }
465 line.clear();
466 } else {
467 line.append(c);
468 }
469 }
470 if (p == pEnd) {
471 qWarning(msg: "Malformed HDR image data at property strings");
472 return imageData;
473 }
474
475 // Get the resolution string.
476 while (p < pEnd) {
477 char c = *p++;
478 if (c == '\n')
479 break;
480 line.append(c);
481 }
482 if (p == pEnd) {
483 qWarning(msg: "Malformed HDR image data at resolution string");
484 return imageData;
485 }
486
487 int width = 0;
488 int height = 0;
489 // We only care about the standard orientation.
490#ifdef Q_CC_MSVC
491 if (!sscanf_s(line.constData(), "-Y %d +X %d", &height, &width)) {
492#else
493 if (!sscanf(s: line.constData(), format: "-Y %d +X %d", &height, &width)) {
494#endif
495 qWarning(msg: "Unsupported HDR resolution string '%s'", line.constData());
496 return imageData;
497 }
498 if (width <= 0 || height <= 0) {
499 qWarning(msg: "Invalid HDR resolution");
500 return imageData;
501 }
502
503 const int bytesPerPixel = format.getSizeofFormat();
504 const int bitCount = bytesPerPixel * 8;
505 const int pitch = calculatePitch(line: calculateLine(width, bitdepth: bitCount));
506 const quint32 dataSize = quint32(height * pitch);
507 imageData = new QSSGLoadedTexture;
508 imageData->dataSizeInBytes = dataSize;
509 imageData->data = ::malloc(size: dataSize);
510 imageData->width = width;
511 imageData->height = height;
512 imageData->format = format;
513 imageData->components = format.getNumberOfComponent();
514 imageData->isSRGB = false;
515
516 // Allocate a scanline worth of RGBE data
517 RGBE *scanline = new RGBE[width];
518
519 // Note we are writing to the data buffer from bottom to top
520 // to correct for -Y orientation
521 for (int y = 0; y < height; ++y) {
522 quint32 byteOffset = quint32((height - 1 - y) * width * bytesPerPixel);
523 if (pEnd - p < 4) {
524 qWarning(msg: "Unexpected end of HDR data");
525 delete[] scanline;
526 return imageData;
527 }
528 decrunchScanline(p, pEnd, scanline, w: width);
529 decodeScanlineToTexture(scanline, width, outBuf: imageData->data, offset: byteOffset, inFormat: format);
530 }
531
532 delete[] scanline;
533 }
534
535 return imageData;
536}
537
538QSSGLoadedTexture *loadExr(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat format)
539{
540 QSSGLoadedTexture *imageData = nullptr;
541
542 char versionBuffer[tinyexr::kEXRVersionSize];
543 source->seek(pos: 0);
544 auto size = source->read(data: versionBuffer, maxlen: tinyexr::kEXRVersionSize);
545 // Check if file is big enough
546 if (size != tinyexr::kEXRVersionSize)
547 return imageData;
548 // Try to load the Version
549 EXRVersion exrVersion;
550 if (ParseEXRVersionFromMemory(version: &exrVersion, memory: reinterpret_cast<unsigned char *>(versionBuffer), size: tinyexr::kEXRVersionSize) != TINYEXR_SUCCESS)
551 return imageData;
552
553 // Check that the file is not a multipart file
554 if (exrVersion.multipart)
555 return imageData;
556
557 // If we get here, than this is an EXR file
558 source->seek(pos: 0);
559 QByteArray buf = source->readAll();
560 const char *err = nullptr;
561 // Header
562 EXRHeader exrHeader;
563 InitEXRHeader(exr_header: &exrHeader);
564 if (ParseEXRHeaderFromMemory(exr_header: &exrHeader,
565 version: &exrVersion,
566 memory: reinterpret_cast<const unsigned char *>(buf.constData()),
567 size: buf.size(),
568 err: &err) != TINYEXR_SUCCESS) {
569 qWarning(msg: "Failed to parse EXR Header with error: '%s'", err);
570 FreeEXRErrorMessage(msg: err);
571 return imageData;
572 }
573
574 // Make sure we get floats instead of half floats
575 for (int i = 0; i < exrHeader.num_channels; i++) {
576 if (exrHeader.pixel_types[i] == TINYEXR_PIXELTYPE_HALF)
577 exrHeader.requested_pixel_types[i] = TINYEXR_PIXELTYPE_FLOAT;
578 }
579
580 // Image
581 EXRImage exrImage;
582
583 InitEXRImage(exr_image: &exrImage);
584 if (LoadEXRImageFromMemory(exr_image: &exrImage,
585 exr_header: &exrHeader,
586 memory: reinterpret_cast<const unsigned char *>(buf.constData()),
587 size: buf.size(),
588 err: &err) != TINYEXR_SUCCESS) {
589 qWarning(msg: "Failed to load EXR Image with error: '%s'", err);
590 FreeEXRHeader(exr_header: &exrHeader);
591 FreeEXRErrorMessage(msg: err);
592 return imageData;
593 }
594
595 // Setup Output container
596 const int bytesPerPixel = format.getSizeofFormat();
597 const int bitCount = bytesPerPixel * 8;
598 const int pitch = calculatePitch(line: calculateLine(width: exrImage.width, bitdepth: bitCount));
599 const quint32 dataSize = quint32(exrImage.height * pitch);
600 imageData = new QSSGLoadedTexture;
601 imageData->dataSizeInBytes = dataSize;
602 imageData->data = ::malloc(size: dataSize);
603 imageData->width = exrImage.width;
604 imageData->height = exrImage.height;
605 imageData->format = format;
606 imageData->components = format.getNumberOfComponent();
607 imageData->isSRGB = false;
608
609 quint8 *target = reinterpret_cast<quint8 *>(imageData->data);
610
611 // Convert data
612 // RGBA
613 int idxR = -1;
614 int idxG = -1;
615 int idxB = -1;
616 int idxA = -1;
617 for (int c = 0; c < exrHeader.num_channels; c++) {
618 if (strcmp(s1: exrHeader.channels[c].name, s2: "R") == 0)
619 idxR = c;
620 else if (strcmp(s1: exrHeader.channels[c].name, s2: "G") == 0)
621 idxG = c;
622 else if (strcmp(s1: exrHeader.channels[c].name, s2: "B") == 0)
623 idxB = c;
624 else if (strcmp(s1: exrHeader.channels[c].name, s2: "A") == 0)
625 idxA = c;
626 }
627 const bool isSingleChannel = exrHeader.num_channels == 1;
628 float rgbaF32[4];
629
630 if (exrHeader.tiled) {
631 for (int it = 0; it < exrImage.num_tiles; it++) {
632 for (int j = 0; j < exrHeader.tile_size_y; j++)
633 for (int i = 0; i < exrHeader.tile_size_x; i++) {
634 const int ii =
635 exrImage.tiles[it].offset_x * exrHeader.tile_size_x + i;
636 const int jj =
637 exrImage.tiles[it].offset_y * exrHeader.tile_size_y + j;
638 const int inverseJJ = std::abs(x: jj - (exrImage.height - 1));
639 const int idx = ii + inverseJJ * exrImage.width;
640
641 // out of region check.
642 if (ii >= exrImage.width) {
643 continue;
644 }
645 if (jj >= exrImage.height) {
646 continue;
647 }
648 const int srcIdx = i + j * exrHeader.tile_size_x;
649 unsigned char **src = exrImage.tiles[it].images;
650 if (isSingleChannel) {
651 rgbaF32[R] = reinterpret_cast<float **>(src)[0][srcIdx];
652 rgbaF32[G] = rgbaF32[R];
653 rgbaF32[B] = rgbaF32[R];
654 rgbaF32[3] = rgbaF32[R];
655 } else {
656 rgbaF32[R] = reinterpret_cast<float **>(src)[idxR][srcIdx];
657 rgbaF32[G] = reinterpret_cast<float **>(src)[idxG][srcIdx];
658 rgbaF32[B] = reinterpret_cast<float **>(src)[idxB][srcIdx];
659 if (idxA != -1)
660 rgbaF32[3] = reinterpret_cast<float **>(src)[idxA][srcIdx];
661 else
662 rgbaF32[3] = 1.0f;
663 }
664 format.encodeToPixel(inPtr: rgbaF32, outPtr: target, byteOfs: idx * bytesPerPixel);
665 }
666 }
667 } else {
668 int idx = 0;
669 for (int y = exrImage.height - 1; y >= 0; --y) {
670 for (int x = 0; x < exrImage.width; x++) {
671 const int i = y * exrImage.width + x;
672 if (isSingleChannel) {
673 rgbaF32[R] = reinterpret_cast<float **>(exrImage.images)[0][i];
674 rgbaF32[G] = rgbaF32[R];
675 rgbaF32[B] = rgbaF32[R];
676 rgbaF32[3] = rgbaF32[R];
677 } else {
678 rgbaF32[R] = reinterpret_cast<float **>(exrImage.images)[idxR][i];
679 rgbaF32[G] = reinterpret_cast<float **>(exrImage.images)[idxG][i];
680 rgbaF32[B] = reinterpret_cast<float **>(exrImage.images)[idxB][i];
681 if (idxA != -1)
682 rgbaF32[3] = reinterpret_cast<float **>(exrImage.images)[idxA][i];
683 else
684 rgbaF32[3] = 1.0f;
685 }
686 format.encodeToPixel(inPtr: rgbaF32, outPtr: target, byteOfs: idx * bytesPerPixel);
687 ++idx;
688 }
689 }
690 }
691
692 // Cleanup
693 FreeEXRImage(exr_image: &exrImage);
694 FreeEXRHeader(exr_header: &exrHeader);
695
696 return imageData;
697
698}
699}
700
701QSSGLoadedTexture *QSSGLoadedTexture::loadHdrImage(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat &inFormat)
702{
703 QSSGLoadedTexture *imageData = nullptr;
704 // We need to do a sanity check on the inFormat
705 QSSGRenderTextureFormat format = inFormat;
706 if (format.format == QSSGRenderTextureFormat::Unknown) {
707 // Loading HDR images for use outside of lightProbes will end up here
708 // The renderer doesn't understand RGBE8 textures outside of lightProbes
709 // So this needs to be a "real" format
710 // TODO: This is a fallback, but there is no way of telling here what formats are supported
711 format = QSSGRenderTextureFormat::RGBA16F;
712 }
713
714 // .hdr Files
715 imageData = loadRadianceHdr(source, format);
716
717 // .exr Files
718 if (!imageData)
719 imageData = loadExr(source, format);
720
721 return imageData;
722}
723
724QSSGLoadedTexture *QSSGLoadedTexture::loadTextureData(QSSGRenderTextureData *textureData)
725{
726 QSSGLoadedTexture *imageData = new QSSGLoadedTexture;
727
728 if (!textureData->format().isCompressedTextureFormat()) {
729 const int bytesPerPixel = textureData->format().getSizeofFormat();
730 const int bitCount = bytesPerPixel * 8;
731 const int pitch = calculatePitch(line: calculateLine(width: textureData->size().width(), bitdepth: bitCount));
732 quint32 dataSize = quint32(textureData->size().height() * pitch);
733 if (textureData->depth() > 0)
734 dataSize *= textureData->depth();
735 imageData->dataSizeInBytes = dataSize;
736 // We won't modifiy the data, but that is a nasty cast...
737 imageData->data = const_cast<void*>(reinterpret_cast<const void*>(textureData->textureData().data()));
738 imageData->width = textureData->size().width();
739 imageData->height = textureData->size().height();
740 imageData->depth = textureData->depth();
741 imageData->format = textureData->format();
742 imageData->components = textureData->format().getNumberOfComponent();
743 } else {
744 // Compressed Textures work a bit differently
745 // Fill out what makes sense, leave the rest at the default 0 and null.
746 imageData->data = const_cast<void*>(reinterpret_cast<const void*>(textureData->textureData().data()));
747 imageData->dataSizeInBytes = textureData->textureData().size();
748 // When we use depth we need to do slicing per layer for the uploads, but right now there it is non-trivial
749 // to determine the size of each "pixel" for compressed formats, so we don't support it for now.
750 // TODO: We need to force depth to 0 for now, as we don't support compressed 3D textures from texureData
751 imageData->width = textureData->size().width();
752 imageData->height = textureData->size().height();
753 imageData->format = textureData->format();
754 }
755
756 // #TODO: add an API to make this explicit
757 // For now we assume HDR formats are linear and everything else
758 // is sRGB, which is not ideal but so far this is only used by
759 // the environment mapper code
760 if (imageData->format == QSSGRenderTextureFormat::RGBE8 ||
761 imageData->format == QSSGRenderTextureFormat::RGBA16F ||
762 imageData->format == QSSGRenderTextureFormat::RGBA32F ||
763 imageData->format == QSSGRenderTextureFormat::BC6H)
764 imageData->isSRGB = false;
765 else
766 imageData->isSRGB = true;
767
768 return imageData;
769}
770
771namespace {
772
773bool scanImageForAlpha(const void *inData, quint32 inWidth, quint32 inHeight, quint32 inPixelSizeInBytes, quint8 inAlphaSizeInBits)
774{
775 const quint8 *rowPtr = reinterpret_cast<const quint8 *>(inData);
776 bool hasAlpha = false;
777 if (inAlphaSizeInBits == 0)
778 return hasAlpha;
779 if (inPixelSizeInBytes != 2 && inPixelSizeInBytes != 4) {
780 Q_ASSERT(false);
781 return false;
782 }
783 if (inAlphaSizeInBits > 8) {
784 Q_ASSERT(false);
785 return false;
786 }
787
788 quint32 alphaRightShift = inPixelSizeInBytes * 8 - inAlphaSizeInBits;
789 quint32 maxAlphaValue = (1 << inAlphaSizeInBits) - 1;
790
791 for (quint32 rowIdx = 0; rowIdx < inHeight && !hasAlpha; ++rowIdx) {
792 for (quint32 idx = 0; idx < inWidth && !hasAlpha; ++idx, rowPtr += inPixelSizeInBytes) {
793 quint32 pixelValue = 0;
794 if (inPixelSizeInBytes == 2)
795 pixelValue = *(reinterpret_cast<const quint16 *>(rowPtr));
796 else
797 pixelValue = *(reinterpret_cast<const quint32 *>(rowPtr));
798 pixelValue = pixelValue >> alphaRightShift;
799 if (pixelValue < maxAlphaValue)
800 hasAlpha = true;
801 }
802 }
803 return hasAlpha;
804}
805}
806
807QSSGLoadedTexture::~QSSGLoadedTexture()
808{
809 if (data && image.sizeInBytes() <= 0 && ownsData)
810 ::free(ptr: data);
811}
812
813bool QSSGLoadedTexture::scanForTransparency() const
814{
815 switch (format.format) {
816 case QSSGRenderTextureFormat::SRGB8A8:
817 case QSSGRenderTextureFormat::RGBA8:
818 if (!data) // dds
819 return true;
820
821 return scanImageForAlpha(inData: data, inWidth: width, inHeight: height, inPixelSizeInBytes: 4, inAlphaSizeInBits: 8);
822 // Scan the image.
823 case QSSGRenderTextureFormat::SRGB8:
824 case QSSGRenderTextureFormat::RGB8:
825 case QSSGRenderTextureFormat::RGBE8:
826 return false;
827 case QSSGRenderTextureFormat::RGB565:
828 return false;
829 case QSSGRenderTextureFormat::RGBA5551:
830 if (!data) { // dds
831 return true;
832 } else {
833 return scanImageForAlpha(inData: data, inWidth: width, inHeight: height, inPixelSizeInBytes: 2, inAlphaSizeInBits: 1);
834 }
835 case QSSGRenderTextureFormat::Alpha8:
836 return true;
837 case QSSGRenderTextureFormat::R8:
838 case QSSGRenderTextureFormat::Luminance8:
839 case QSSGRenderTextureFormat::RG8:
840 return false;
841 case QSSGRenderTextureFormat::LuminanceAlpha8:
842 if (!data) // dds
843 return true;
844
845 return scanImageForAlpha(inData: data, inWidth: width, inHeight: height, inPixelSizeInBytes: 2, inAlphaSizeInBits: 8);
846 case QSSGRenderTextureFormat::RGB_DXT1:
847 return false;
848 case QSSGRenderTextureFormat::RGBA_DXT3:
849 case QSSGRenderTextureFormat::RGBA_DXT1:
850 case QSSGRenderTextureFormat::RGBA_DXT5:
851 return false;
852 case QSSGRenderTextureFormat::RGB9E5:
853 return false;
854 case QSSGRenderTextureFormat::RG32F:
855 case QSSGRenderTextureFormat::RGB32F:
856 case QSSGRenderTextureFormat::RGBA16F:
857 case QSSGRenderTextureFormat::RGBA32F:
858 // TODO : For now, since IBL will be the main consumer, we'll just
859 // pretend there's no alpha. Need to do a proper scan down the line,
860 // but doing it for floats is a little different from integer scans.
861 return false;
862 default:
863 break;
864 }
865 Q_ASSERT(false);
866 return false;
867}
868
869static bool isCompatible(const QImage &img1, const QImage &img2)
870{
871 if (img1.size() != img2.size())
872 return false;
873 if (img1.pixelFormat().channelCount() != img2.pixelFormat().channelCount())
874 return false;
875
876 return true;
877}
878
879static QSSGLoadedTexture *loadCubeMap(const QString &inPath, bool flipY)
880{
881 QStringList fileNames;
882 if (inPath.contains(QStringLiteral("%p"))) {
883 fileNames.reserve(asize: 6);
884 const char *faces[6] = { "posx", "negx", "posy", "negy", "posz", "negz" };
885 for (const auto face : faces) {
886 QString fileName = inPath;
887 fileName.replace(QStringLiteral("%p"), after: QLatin1StringView(face));
888 fileNames << fileName;
889 }
890
891 } else if (inPath.contains(QStringLiteral(";"))) {
892 fileNames = inPath.split(sep: QChar(u';'));
893 }
894 if (fileNames.size() != 6)
895 return nullptr; // TODO: allow sparse cube maps (with some faces missing)
896 std::unique_ptr<QTextureFileData> textureFileData = std::make_unique<QTextureFileData>(args: QTextureFileData::ImageMode);
897 textureFileData->setNumFaces(6);
898 textureFileData->setNumLevels(1);
899 textureFileData->setLogName(inPath.toUtf8());
900 QImage prevImage;
901 for (int i = 0; i < 6; ++i) {
902 QString searchName = fileNames[i];
903 QString filePath;
904 auto stream = QSSGInputUtil::getStreamForFile(inPath: searchName, inQuiet: true, outPath: &filePath);
905 if (!stream)
906 return nullptr;
907
908 QImage face = loadImage(inPath: filePath, flipVertical: !flipY); // Cube maps are flipped the other way
909 if (face.isNull() || (!prevImage.isNull() && !isCompatible(img1: prevImage, img2: face))) {
910 return nullptr;
911 }
912 textureFileData->setData(image: face, level: 0, face: i);
913 textureFileData->setSize(face.size());
914 prevImage = face;
915 }
916
917 QSSGLoadedTexture *retval = new QSSGLoadedTexture;
918
919 retval->textureFileData = *textureFileData;
920
921 retval->width = prevImage.width();
922 retval->height = prevImage.height();
923 retval->components = prevImage.pixelFormat().channelCount();
924 retval->image = prevImage;
925 retval->data = (void *)retval->image.bits();
926 retval->dataSizeInBytes = prevImage.sizeInBytes();
927 retval->setFormatFromComponents();
928 // #TODO: This is a very crude way detect color space
929 retval->isSRGB = prevImage.colorSpace().transferFunction() != QColorSpace::TransferFunction::Linear;
930
931 return retval;
932}
933
934QSSGLoadedTexture *QSSGLoadedTexture::load(const QString &inPath,
935 const QSSGRenderTextureFormat &inFormat,
936 bool inFlipY)
937{
938 if (inPath.isEmpty())
939 return nullptr;
940
941 QSSGLoadedTexture *theLoadedImage = nullptr;
942 QString fileName;
943 QSSGInputUtil::FileType fileType = QSSGInputUtil::UnknownFile;
944 QSharedPointer<QIODevice> theStream =
945 QSSGInputUtil::getStreamForTextureFile(inPath, inQuiet: true, outPath: &fileName, outFileType: &fileType);
946
947 if (theStream) {
948 switch (fileType) {
949 case QSSGInputUtil::HdrFile:
950 // inFormat is a suggestion that's only relevant for HDR images
951 // (tells if we want want RGBA16F or RGBE-on-RGBA8)
952 theLoadedImage = loadHdrImage(source: theStream, inFormat);
953 break;
954 case QSSGInputUtil::TextureFile:
955 theLoadedImage = loadCompressedImage(inPath: fileName); // no choice but to ignore inFlipY here
956 break;
957 default:
958 theLoadedImage = loadQImage(inPath: fileName, flipVertical: inFlipY);
959 break;
960 }
961 } else {
962 // Check to see if we can find a cubemap
963 return loadCubeMap(inPath, flipY: inFlipY);
964 }
965 return theLoadedImage;
966}
967
968QT_END_NAMESPACE
969

source code of qtquick3d/src/runtimerender/resourcemanager/qssgrenderloadedtexture.cpp