SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
VTF.cpp
Go to the documentation of this file.
1#include <vtfpp/VTF.h>
2
3#include <algorithm>
4#include <cstring>
5#include <unordered_map>
6#include <utility>
7
8#ifdef SOURCEPP_BUILD_WITH_TBB
9#include <execution>
10#endif
11
12#ifdef SOURCEPP_BUILD_WITH_THREADS
13#include <future>
14#include <thread>
15#endif
16
17#include <BufferStream.h>
18#include <miniz.h>
19#include <zstd.h>
20
23
24using namespace sourcepp;
25using namespace vtfpp;
26
27namespace {
28
29[[nodiscard]] std::vector<std::byte> compressData(std::span<const std::byte> data, int16_t level, CompressionMethod method) {
30 switch (method) {
31 using enum CompressionMethod;
32 case DEFLATE: {
33 mz_ulong compressedSize = mz_compressBound(data.size());
34 std::vector<std::byte> out(compressedSize);
35
36 int status = MZ_OK;
37 while ((status = mz_compress2(reinterpret_cast<unsigned char*>(out.data()), &compressedSize, reinterpret_cast<const unsigned char*>(data.data()), data.size(), level)) == MZ_BUF_ERROR) {
38 compressedSize *= 2;
39 out.resize(compressedSize);
40 }
41
42 if (status != MZ_OK) {
43 return {};
44 }
45 out.resize(compressedSize);
46 return out;
47 }
48 case ZSTD: {
49 if (level < 0) {
50 level = 6;
51 }
52
53 const auto expectedSize = ZSTD_compressBound(data.size());
54 std::vector<std::byte> out(expectedSize);
55
56 const auto compressedSize = ZSTD_compress(out.data(), expectedSize, data.data(), data.size(), level);
57 if (ZSTD_isError(compressedSize)) {
58 return {};
59 }
60
61 out.resize(compressedSize);
62 return out;
63 }
64 case CONSOLE_LZMA: {
65 if (const auto out = compression::compressValveLZMA(data, level)) {
66 return *out;
67 }
68 return {};
69 }
70 }
71 return {};
72}
73
74template<bool ExistingDataIsSwizzled>
75constexpr void swizzleImageDataForPS3(std::span<std::byte> inputData, std::span<std::byte> outputData, ImageFormat format, uint16_t width, uint16_t height, uint16_t slice) {
76 width *= ImageFormatDetails::bpp(format) / 32;
77 const auto zIndex = [
78 widthL2 = static_cast<int>(math::log2ceil(width)),
79 heightL2 = static_cast<int>(math::log2ceil(height)),
80 sliceL2 = static_cast<int>(math::log2ceil(slice))
81 ](uint32_t x, uint32_t y, uint32_t z) {
82 auto widthL2m = widthL2;
83 auto heightL2m = heightL2;
84 auto sliceL2m = sliceL2;
85 uint32_t offset = 0;
86 uint32_t shiftCount = 0;
87 do {
88 if (sliceL2m --> 0) {
89 offset |= (z & 1) << shiftCount++;
90 z >>= 1;
91 }
92 if (heightL2m --> 0) {
93 offset |= (y & 1) << shiftCount++;
94 y >>= 1;
95 }
96 if (widthL2m --> 0) {
97 offset |= (x & 1) << shiftCount++;
98 x >>= 1;
99 }
100 } while (x | y | z);
101 return offset;
102 };
103
104 const auto* inputPtr = reinterpret_cast<const uint32_t*>(inputData.data());
105 auto* outputPtr = reinterpret_cast<uint32_t*>(outputData.data());
106 for (uint16_t x = 0; x < width; x++) {
107 for (uint16_t y = 0; y < height; y++) {
108 for (uint16_t z = 0; z < slice; z++) {
109 if constexpr (ExistingDataIsSwizzled) {
110 *outputPtr++ = reinterpret_cast<const uint32_t*>(inputData.data())[zIndex(x, y, z)];
111 } else {
112 reinterpret_cast<uint32_t*>(outputData.data())[zIndex(x, y, z)] = *inputPtr++;
113 }
114 }
115 }
116 }
117}
118
119template<bool ConvertingFromSource>
120void swapImageDataEndianForConsole(std::span<std::byte> imageData, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount, VTF::Platform platform) {
121 if (imageData.empty() || format == ImageFormat::EMPTY || platform == VTF::PLATFORM_PC) {
122 return;
123 }
124
125 if (platform == VTF::PLATFORM_X360) {
126 switch (format) {
127 using enum ImageFormat;
128 case BGRA8888:
129 case BGRX8888:
130 case UVWQ8888:
131 case UVLX8888: {
132 const auto newData = ImageConversion::convertSeveralImageDataToFormat(imageData, ARGB8888, BGRA8888, mipCount, frameCount, faceCount, width, height, sliceCount);
133 std::ranges::copy(newData, imageData.begin());
134 break;
135 }
136 case DXT1:
138 case DXT3:
139 case DXT5:
140 case UV88: {
141 std::span dxtData{reinterpret_cast<uint16_t*>(imageData.data()), imageData.size() / sizeof(uint16_t)};
142 std::for_each(
143#ifdef SOURCEPP_BUILD_WITH_TBB
144 std::execution::par_unseq,
145#endif
146 dxtData.begin(), dxtData.end(), [](uint16_t& value) {
147 BufferStream::swap_endian(&value);
148 });
149 break;
150 }
151 default:
152 break;
153 }
154 }
155
156 if ((platform == VTF::PLATFORM_PS3_ORANGEBOX || platform == VTF::PLATFORM_PS3_PORTAL2) && !ImageFormatDetails::compressed(format) && ImageFormatDetails::bpp(format) % 32 == 0) {
157 std::vector<std::byte> out(imageData.size());
158 for(int mip = mipCount - 1; mip >= 0; mip--) {
159 const auto mipWidth = ImageDimensions::getMipDim(mip, width);
160 const auto mipHeight = ImageDimensions::getMipDim(mip, height);
161 for (int frame = 0; frame < frameCount; frame++) {
162 for (int face = 0; face < faceCount; face++) {
163 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, format, mip, mipCount, frame, frameCount, face, faceCount, width, height)) {
164 std::span imageDataSpan{imageData.data() + offset, length * sliceCount};
165 std::span outSpan{out.data() + offset, length * sliceCount};
166 ::swizzleImageDataForPS3<ConvertingFromSource>(imageDataSpan, outSpan, format, mipWidth, mipHeight, sliceCount);
167 }
168 }
169 }
170 }
171 std::memcpy(imageData.data(), out.data(), out.size());
172 }
173}
174
175} // namespace
176
178 switch (this->type) {
180 if (this->data.size() <= sizeof(uint32_t)) {
181 return {};
182 }
183 return SHT{{reinterpret_cast<const std::byte*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data())}};
184 case TYPE_CRC:
186 if (this->data.size() != sizeof(uint32_t)) {
187 return {};
188 }
189 return *reinterpret_cast<const uint32_t*>(this->data.data());
191 if (this->data.size() != sizeof(uint32_t)) {
192 return {};
193 }
194 return std::make_tuple(
195 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 0),
196 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 1),
197 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 2),
198 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 3));
200 if (this->data.size() <= sizeof(uint32_t)) {
201 return "";
202 }
203 return std::string(reinterpret_cast<const char*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data()));
205 if (this->data.size() <= sizeof(uint32_t)) {
206 return {};
207 }
208 return HOT{{reinterpret_cast<const std::byte*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data())}};
210 if (this->data.size() <= sizeof(uint32_t) || this->data.size() % sizeof(uint32_t) != 0) {
211 return {};
212 }
213 return std::span{reinterpret_cast<uint32_t*>(this->data.data()), this->data.size() / 4};
214 default:
215 break;
216 }
217 return {};
218}
219
221 this->version = 4;
222
223 this->flags |= FLAG_NO_MIP | FLAG_NO_LOD;
224
225 this->format = ImageFormat::EMPTY;
227
228 this->width = 0;
229 this->height = 0;
230 this->mipCount = 0;
231 this->frameCount = 0;
232 this->sliceCount = 0;
233
234 this->opened = true;
235}
236
237VTF::VTF(std::vector<std::byte>&& vtfData, bool parseHeaderOnly)
238 : data(std::move(vtfData)) {
239 BufferStreamReadOnly stream{this->data};
240
241 if (const auto signature = stream.read<uint32_t>(); signature == VTF_SIGNATURE) {
242 stream >> this->platform;
243 if (this->platform != PLATFORM_PC) {
244 return;
245 }
246 stream >> this->version;
247 if (this->version > 6) {
248 return;
249 }
250 } else if (signature == VTFX_SIGNATURE || signature == VTF3_SIGNATURE) {
251 stream.set_big_endian(true);
252 stream >> this->platform;
253 if (this->platform != PLATFORM_X360 && this->platform != PLATFORM_PS3_ORANGEBOX && this->platform != PLATFORM_PS3_PORTAL2) {
254 return;
255 }
256 stream >> this->version;
257 if (this->version != 8) {
258 return;
259 }
260 // Now fix up the actual version as it would be on PC
261 if (signature == VTF3_SIGNATURE) {
262 this->platform = PLATFORM_PS3_PORTAL2;
263 this->version = 5;
264 } else {
265 this->version = 4;
266 }
267 } else {
268 return;
269 }
270
271 const auto headerSize = stream.read<uint32_t>();
272
273 const auto readResources = [this, &stream](uint32_t resourceCount) {
274 // Read resource header info
275 this->resources.reserve(resourceCount);
276 for (int i = 0; i < resourceCount; i++) {
277 auto& [type, flags_, data_] = this->resources.emplace_back();
278
279 auto typeAndFlags = stream.read<uint32_t>();
280 if (stream.is_big_endian()) {
281 // This field is little-endian
282 BufferStream::swap_endian(&typeAndFlags);
283 }
284 type = static_cast<Resource::Type>(typeAndFlags & 0xffffff); // last 3 bytes
285 flags_ = static_cast<Resource::Flags>(typeAndFlags >> 24); // first byte
286 data_ = stream.read_span<std::byte>(4);
287
288 if (stream.is_big_endian() && !(flags_ & Resource::FLAG_LOCAL_DATA)) {
289 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(data_.data()));
290 }
291 }
292
293 // Sort resources by their offset, in case certain VTFs are written
294 // weirdly and have resource data written out of order. So far I have
295 // found only one VTF in an official Valve game where this is the case
296 std::ranges::sort(this->resources, [](const Resource& lhs, const Resource& rhs) {
298 return lhs.type < rhs.type;
299 }
301 return true;
302 }
304 return false;
305 }
306 return *reinterpret_cast<uint32_t*>(lhs.data.data()) < *reinterpret_cast<uint32_t*>(rhs.data.data());
307 });
308
309 // Fix up data spans to point to the actual data
310 Resource* lastResource = nullptr;
311 for (auto& resource : this->resources) {
312 if (!(resource.flags & Resource::FLAG_LOCAL_DATA)) {
313 if (lastResource) {
314 const auto lastOffset = *reinterpret_cast<uint32_t*>(lastResource->data.data());
315 const auto currentOffset = *reinterpret_cast<uint32_t*>(resource.data.data());
316 const auto curPos = stream.tell();
317 stream.seek(lastOffset);
318 lastResource->data = stream.read_span<std::byte>(currentOffset - lastOffset);
319 stream.seek(static_cast<int64_t>(curPos));
320 }
321 lastResource = &resource;
322 }
323 }
324 if (lastResource) {
325 auto offset = *reinterpret_cast<uint32_t*>(lastResource->data.data());
326 auto curPos = stream.tell();
327 stream.seek(offset);
328 lastResource->data = stream.read_span<std::byte>(stream.size() - offset);
329 stream.seek(static_cast<int64_t>(curPos));
330 }
331 };
332
333 switch (this->platform) {
334 case PLATFORM_UNKNOWN:
335 return;
336 case PLATFORM_PC: {
337 stream
338 .read(this->width)
339 .read(this->height)
340 .read(this->flags)
341 .read(this->frameCount)
342 .read(this->startFrame)
343 .skip(4)
344 .read(this->reflectivity[0])
345 .read(this->reflectivity[1])
346 .read(this->reflectivity[2])
347 .skip(4)
348 .read(this->bumpMapScale)
349 .read(this->format)
350 .read(this->mipCount);
351
352 // This will always be DXT1
353 stream.skip<ImageFormat>();
354 stream >> this->thumbnailWidth >> this->thumbnailHeight;
355 if (this->thumbnailWidth == 0 || this->thumbnailHeight == 0) {
357 } else {
359 }
360
361 if (this->version < 2) {
362 this->sliceCount = 1;
363 } else {
364 stream.read(this->sliceCount);
365 }
366
367 if (parseHeaderOnly) {
368 this->opened = true;
369 return;
370 }
371
372 if (this->version >= 3) {
373 stream.skip(3);
374 auto resourceCount = stream.read<uint32_t>();
375 stream.skip(8);
376 readResources(resourceCount);
377
378 this->opened = stream.tell() == headerSize;
379
380 if (this->opened && this->version >= 6) {
381 const auto* auxResource = this->getResource(Resource::TYPE_AUX_COMPRESSION);
382 const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA);
383 if (auxResource && imageResource) {
384 if (auxResource->getDataAsAuxCompressionLevel() != 0) {
385 const auto faceCount = this->getFaceCount();
386 std::vector<std::byte> decompressedImageData(ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->sliceCount));
387 uint32_t oldOffset = 0;
388 for (int i = this->mipCount - 1; i >= 0; i--) {
389 for (int j = 0; j < this->frameCount; j++) {
390 for (int k = 0; k < faceCount; k++) {
391 uint32_t oldLength = auxResource->getDataAsAuxCompressionLength(i, this->mipCount, j, this->frameCount, k, faceCount);
392 if (uint32_t newOffset, newLength; ImageFormatDetails::getDataPosition(newOffset, newLength, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->getSliceCount())) {
393 // Keep in mind that slices are compressed together
394 mz_ulong decompressedImageDataSize = newLength * this->sliceCount;
395 switch (auxResource->getDataAsAuxCompressionMethod()) {
396 using enum CompressionMethod;
397 case DEFLATE:
398 if (mz_uncompress(reinterpret_cast<unsigned char*>(decompressedImageData.data() + newOffset), &decompressedImageDataSize, reinterpret_cast<const unsigned char*>(imageResource->data.data() + oldOffset), oldLength) != MZ_OK) {
399 this->opened = false;
400 return;
401 }
402 break;
403 case ZSTD:
404 if (auto decompressedSize = ZSTD_decompress(reinterpret_cast<unsigned char*>(decompressedImageData.data() + newOffset), decompressedImageDataSize, reinterpret_cast<const unsigned char*>(imageResource->data.data() + oldOffset), oldLength); ZSTD_isError(decompressedSize) || decompressedSize != decompressedImageDataSize) {
405 this->opened = false;
406 return;
407 }
408 break;
409 case CONSOLE_LZMA:
410 // Shouldn't be here!
412 break;
413 }
414 }
415 oldOffset += oldLength;
416 }
417 }
418 }
419 this->setResourceInternal(Resource::TYPE_IMAGE_DATA, decompressedImageData);
420 }
421 }
422 }
423 } else {
424 stream.skip(math::paddingForAlignment(16, stream.tell()));
425 this->opened = stream.tell() == headerSize;
426
427 this->resources.reserve(2);
428
429 if (this->hasThumbnailData()) {
430 this->resources.push_back({
432 .flags = Resource::FLAG_NONE,
433 .data = stream.read_span<std::byte>(ImageFormatDetails::getDataLength(this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight)),
434 });
435 }
436 if (this->hasImageData()) {
437 this->resources.push_back({
439 .flags = Resource::FLAG_NONE,
440 .data = stream.read_span<std::byte>(stream.size() - stream.tell()),
441 });
442 }
443 }
444
445 if (const auto* resource = this->getResource(Resource::TYPE_AUX_COMPRESSION)) {
446 this->compressionLevel = resource->getDataAsAuxCompressionLevel();
447 this->compressionMethod = resource->getDataAsAuxCompressionMethod();
449 }
450 break;
451 }
452 case PLATFORM_X360:
455 uint8_t resourceCount;
456 stream
457 .read(this->flags)
458 .read(this->width)
459 .read(this->height)
460 .read(this->sliceCount)
461 .read(this->frameCount)
462 .skip<uint16_t>() // preload
463 .skip<uint8_t>() // skip high mip levels
464 .read(resourceCount)
465 .read(this->reflectivity[0])
466 .read(this->reflectivity[1])
467 .read(this->reflectivity[2])
468 .read(this->bumpMapScale)
469 .read(this->format)
470 .skip<math::Vec4ui8>() // lowResImageSample (replacement for thumbnail resource, presumably linear color)
471 .skip<uint32_t>(); // compressedLength
472
473 // Align to 16 bytes
474 if (this->platform == PLATFORM_PS3_PORTAL2) {
475 stream.skip<uint32_t>();
476 }
477
478 this->mipCount = (this->flags & FLAG_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height);
479
480 if (parseHeaderOnly) {
481 this->opened = true;
482 return;
483 }
484
485 this->resources.reserve(resourceCount);
486 readResources(resourceCount);
487
488 this->opened = stream.tell() == headerSize;
489
490 // The resources vector isn't modified by setResourceInternal when we're not adding a new one, so this is fine
491 for (const auto& resource : this->resources) {
492 // Decompress LZMA resources
493 if (BufferStreamReadOnly rsrcStream{resource.data.data(), resource.data.size()}; rsrcStream.read<uint32_t>() == compression::VALVE_LZMA_SIGNATURE) {
494 if (auto decompressedData = compression::decompressValveLZMA(resource.data)) {
495 this->setResourceInternal(resource.type, *decompressedData);
496
497 if (resource.type == Resource::TYPE_IMAGE_DATA) {
498 // Do this here because compressionLength in header can be garbage on PS3 orange box
500 }
501 }
502 }
503
504 if (resource.type == Resource::TYPE_THUMBNAIL_DATA) {
505 ::swapImageDataEndianForConsole<true>(resource.data, this->thumbnailFormat, 1, 1, 1, this->thumbnailWidth, this->thumbnailHeight, 1, this->platform);
506 } else if (resource.type == Resource::TYPE_IMAGE_DATA) {
507 ::swapImageDataEndianForConsole<true>(resource.data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->sliceCount, this->platform);
508 } else if (!(resource.flags & Resource::FLAG_LOCAL_DATA) && resource.data.size() >= sizeof(uint32_t)) {
509 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(resource.data.data()));
510 }
511 }
512 break;
513 }
514 }
515}
516
517VTF::VTF(std::span<const std::byte> vtfData, bool parseHeaderOnly)
518 : VTF(std::vector<std::byte>{vtfData.begin(), vtfData.end()}, parseHeaderOnly) {}
519
520VTF::VTF(const std::string& vtfPath, bool parseHeaderOnly)
521 : VTF(fs::readFileBuffer(vtfPath), parseHeaderOnly) {}
522
523VTF::VTF(const VTF& other) {
524 *this = other;
525}
526
527VTF& VTF::operator=(const VTF& other) {
528 this->opened = other.opened;
529 this->data = other.data;
530 this->version = other.version;
531 this->width = other.width;
532 this->height = other.height;
533 this->flags = other.flags;
534 this->frameCount = other.frameCount;
535 this->startFrame = other.startFrame;
536 this->reflectivity = other.reflectivity;
537 this->bumpMapScale = other.bumpMapScale;
538 this->format = other.format;
539 this->mipCount = other.mipCount;
540 this->thumbnailFormat = other.thumbnailFormat;
541 this->thumbnailWidth = other.thumbnailWidth;
542 this->thumbnailHeight = other.thumbnailHeight;
543 this->sliceCount = other.sliceCount;
544
545 this->resources.clear();
546 for (const auto& [otherType, otherFlags, otherData] : other.resources) {
547 auto& [type, flags_, data_] = this->resources.emplace_back();
548 type = otherType;
549 flags_ = otherFlags;
550 data_ = {this->data.data() + (otherData.data() - other.data.data()), otherData.size()};
551 }
552
553 this->platform = other.platform;
558
559 return *this;
560}
561
562VTF::operator bool() const {
563 return this->opened;
564}
565
567 bool out = true;
568 if (writer.hasImageData() && (options.invertGreenChannel || options.gammaCorrection != 1.f)) {
569 for (int i = 1; i < writer.mipCount; i++) {
570 for (int j = 0; j < writer.frameCount; j++) {
571 for (int k = 0; k < writer.getFaceCount(); k++) {
572 for (int l = 0; l < writer.sliceCount; l++) {
573 if (options.invertGreenChannel && !writer.setImage(ImageConversion::invertGreenChannelForImageData(writer.getImageDataRaw(i, j, k, l), writer.getFormat(), writer.getWidth(i), writer.getHeight(i)), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), ImageConversion::ResizeFilter::DEFAULT, i, j, k, l)) {
574 out = false;
575 }
576 if (options.gammaCorrection != 1.f && !writer.setImage(ImageConversion::gammaCorrectImageData(writer.getImageDataRaw(i, j, k, l), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), options.gammaCorrection), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), ImageConversion::ResizeFilter::DEFAULT, i, j, k, l)) {
577 out = false;
578 }
579 }
580 }
581 }
582 }
583 }
584 writer.setPlatform(options.platform);
585 if (options.computeReflectivity) {
586 writer.computeReflectivity();
587 }
588 if (options.initialFrameCount > 1 || options.isCubeMap || options.initialSliceCount > 1) {
589 if (!writer.setFrameFaceAndSliceCount(options.initialFrameCount, options.isCubeMap, options.initialSliceCount)) {
590 out = false;
591 }
592 }
593 writer.setStartFrame(options.startFrame);
594 writer.setBumpMapScale(options.bumpMapScale);
595 if (options.computeThumbnail) {
596 writer.computeThumbnail();
597 }
598 if (options.outputFormat == VTF::FORMAT_UNCHANGED) {
599 options.outputFormat = writer.getFormat();
600 } else if (options.outputFormat == VTF::FORMAT_DEFAULT) {
601 options.outputFormat = VTF::getDefaultCompressedFormat(writer.getFormat(), writer.getVersion(), options.isCubeMap);
602 }
603 if (options.computeTransparencyFlags) {
605 }
606 if (options.computeMips) {
608 out = false;
609 }
610 writer.computeMips(options.filter);
611 }
612 writer.setFormat(options.outputFormat);
615 return out;
616}
617
618bool VTF::create(std::span<const std::byte> imageData, ImageFormat format, uint16_t width, uint16_t height, const std::string& vtfPath, CreationOptions options) {
619 VTF writer;
620 writer.setVersion(options.version);
621 writer.addFlags(options.flags);
623 if (!writer.setImage(imageData, format, width, height, options.filter)) {
624 return false;
625 }
626 if (!createInternal(writer, options)) {
627 return false;
628 }
629 return writer.bake(vtfPath);
630}
631
632bool VTF::create(ImageFormat format, uint16_t width, uint16_t height, const std::string& vtfPath, CreationOptions options) {
633 std::vector<std::byte> imageData;
634 imageData.resize(static_cast<uint32_t>(width) * height * ImageFormatDetails::bpp(format) / 8);
635 return create(imageData, format, width, height, vtfPath, options);
636}
637
638VTF VTF::create(std::span<const std::byte> imageData, ImageFormat format, uint16_t width, uint16_t height, CreationOptions options) {
639 VTF writer;
640 writer.setVersion(options.version);
641 writer.addFlags(options.flags);
643 writer.setImage(imageData, format, width, height, options.filter);
644 createInternal(writer, options);
645 return writer;
646}
647
648VTF VTF::create(ImageFormat format, uint16_t width, uint16_t height, CreationOptions options) {
649 std::vector<std::byte> imageData;
650 imageData.resize(static_cast<uint32_t>(width) * height * ImageFormatDetails::bpp(format) / 8);
651 return create(imageData, format, width, height, options);
652}
653
654bool VTF::create(const std::string& imagePath, const std::string& vtfPath, CreationOptions options) {
655 VTF writer;
656 writer.setVersion(options.version);
657 writer.addFlags(options.flags);
659 if (!writer.setImage(imagePath, options.filter)) {
660 return false;
661 }
662 if (!createInternal(writer, options)) {
663 return false;
664 }
665 return writer.bake(vtfPath);
666}
667
668VTF VTF::create(const std::string& imagePath, CreationOptions options) {
669 VTF writer;
670 writer.setVersion(options.version);
671 writer.addFlags(options.flags);
673 writer.setImage(imagePath, options.filter);
674 createInternal(writer, options);
675 return writer;
676}
677
679 return this->platform;
680}
681
682void VTF::setPlatform(Platform newPlatform) {
683 if (this->platform == newPlatform) {
684 return;
685 }
686
687 // hack to allow VTF::setVersion to work
688 const auto oldPlatform = this->platform;
689 this->platform = PLATFORM_PC;
690 switch (newPlatform) {
691 case PLATFORM_UNKNOWN:
692 case PLATFORM_PC:
693 break;
694 case PLATFORM_X360:
696 this->setVersion(4);
697 break;
699 this->setVersion(5);
700 break;
701 }
702 this->platform = newPlatform;
704
705 if (this->platform != PLATFORM_PC) {
706 const auto recommendedCount = (this->flags & VTF::FLAG_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height);
707 if (this->mipCount != recommendedCount) {
708 this->setMipCount(recommendedCount);
709 }
710 } else if (oldPlatform != PLATFORM_PC) {
711 const auto recommendedMipCount = ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height);
712 if (this->mipCount > recommendedMipCount) {
713 this->setMipCount(recommendedMipCount);
714 }
715 }
716}
717
718uint32_t VTF::getVersion() const {
719 return this->version;
720}
721
722void VTF::setVersion(uint32_t newVersion) {
723 if (this->platform != PLATFORM_PC) {
724 return;
725 }
726 if (this->hasImageData()) {
727 auto faceCount = this->getFaceCount();
728 if (faceCount == 7 && (newVersion < 1 || newVersion > 4)) {
729 faceCount = 6;
730 }
731 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, faceCount, this->sliceCount);
732 }
733
734 // Fix up flags
735 bool srgb = this->isSRGB();
736 if ((this->version < 2 && newVersion >= 2) || (this->version >= 2 && newVersion < 2)) {
738 }
739 if ((this->version < 3 && newVersion >= 3) || (this->version >= 3 && newVersion < 3)) {
741 }
742 if ((this->version < 4 && newVersion >= 4) || (this->version >= 4 && newVersion < 4)) {
745 }
746 if ((this->version < 5 && newVersion >= 5) || (this->version >= 5 && newVersion < 5)) {
749 }
750 this->setSRGB(srgb);
751
752 this->version = newVersion;
753}
754
756 return this->imageWidthResizeMethod;
757}
758
760 return this->imageHeightResizeMethod;
761}
762
763void VTF::setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMethod_, ImageConversion::ResizeMethod imageHeightResizeMethod_) {
764 this->imageWidthResizeMethod = imageWidthResizeMethod_;
765 this->imageHeightResizeMethod = imageHeightResizeMethod_;
766}
767
769 this->imageWidthResizeMethod = imageWidthResizeMethod_;
770}
771
773 this->imageHeightResizeMethod = imageHeightResizeMethod_;
774}
775
776uint16_t VTF::getWidth(uint8_t mip) const {
777 return mip > 0 ? ImageDimensions::getMipDim(mip, this->width) : this->width;
778}
779
780uint16_t VTF::getHeight(uint8_t mip) const {
781 return mip > 0 ? ImageDimensions::getMipDim(mip, this->height) : this->height;
782}
783
784void VTF::setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter) {
785 if (newWidth == 0 || newHeight == 0) {
786 this->format = ImageFormat::EMPTY;
787 this->width = 0;
788 this->height = 0;
790 return;
791 }
792
794 if (this->hasImageData()) {
795 if (this->width == newWidth && this->height == newHeight) {
796 return;
797 }
798 auto newMipCount = this->mipCount;
799 if (this->platform == VTF::PLATFORM_PC) {
800 if (const auto recommendedCount = ImageDimensions::getRecommendedMipCountForDims(this->format, newWidth, newHeight); newMipCount > recommendedCount) {
801 newMipCount = recommendedCount;
802 }
803 } else {
804 newMipCount = (this->flags & VTF::FLAG_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(newWidth, newHeight);
805 }
806 this->regenerateImageData(this->format, newWidth, newHeight, newMipCount, this->frameCount, this->getFaceCount(), this->sliceCount, filter);
807 } else {
808 this->format = ImageFormat::RGBA8888;
809 this->mipCount = 1;
810 this->frameCount = 1;
811 this->flags &= ~FLAG_ENVMAP;
812 this->width = newWidth;
813 this->height = newHeight;
814 this->sliceCount = 1;
815 this->setResourceInternal(Resource::TYPE_IMAGE_DATA, std::vector<std::byte>(ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, 1, this->width, this->height, this->sliceCount)));
816 }
817}
818
819uint32_t VTF::getFlags() const {
820 return this->flags;
821}
822
823void VTF::setFlags(uint32_t flags_) {
824 this->flags = (this->flags & FLAGS_MASK_INTERNAL) | (flags_ & ~FLAGS_MASK_INTERNAL);
825}
826
827void VTF::addFlags(uint32_t flags_) {
828 this->flags |= flags_ & ~FLAGS_MASK_INTERNAL;
829}
830
831void VTF::removeFlags(uint32_t flags_) {
832 this->flags &= ~flags_ | FLAGS_MASK_INTERNAL;
833}
834
835bool VTF::isSRGB() const {
836 return !ImageFormatDetails::large(this->format) && (this->version < 4 ? false : this->version < 5 ? this->flags & FLAG_V4_SRGB : this->flags & FLAG_V5_PWL_CORRECTED || this->flags & FLAG_V5_SRGB);
837}
838
839void VTF::setSRGB(bool srgb) {
840 if (srgb) {
841 if (this->version >= 5) {
842 this->addFlags(FLAG_V5_SRGB);
843 } else if (this->version >= 4) {
844 this->addFlags(FLAG_V4_SRGB);
845 }
846 } else {
847 if (this->version >= 5) {
849 } else if (this->version >= 4) {
851 }
852 }
853}
854
856 if (ImageFormatDetails::transparent(this->format)) {
857 if (ImageFormatDetails::decompressedAlpha(this->format) > 1) {
860 } else {
863 }
864 } else {
867 }
868}
869
870ImageFormat VTF::getDefaultCompressedFormat(ImageFormat inputFormat, uint32_t version, bool isCubeMap) {
871 if (inputFormat != ImageFormat::EMPTY) {
872 if (version >= 6 && isCubeMap) {
873 return ImageFormat::BC6H;
874 }
875 if (ImageFormatDetails::decompressedAlpha(inputFormat) > 0) {
876 if (version >= 6) {
877 return ImageFormat::BC7;
878 }
879 return ImageFormat::DXT5;
880 }
881 }
882 return ImageFormat::DXT1;
883}
884
886 return this->format;
887}
888
890 if (newFormat == VTF::FORMAT_UNCHANGED || newFormat == this->format) {
891 return;
892 }
893 if (newFormat == VTF::FORMAT_DEFAULT) {
894 newFormat = VTF::getDefaultCompressedFormat(this->format, this->version, this->getFaceCount() > 1);
895 }
896 if (!this->hasImageData()) {
897 this->format = newFormat;
898 return;
899 }
900 auto newMipCount = this->mipCount;
901 if (const auto recommendedCount = ImageDimensions::getRecommendedMipCountForDims(newFormat, this->width, this->height); newMipCount > recommendedCount) {
902 newMipCount = recommendedCount;
903 }
904 if (ImageFormatDetails::compressed(newFormat)) {
905 this->regenerateImageData(newFormat, this->width + math::paddingForAlignment(4, this->width), this->height + math::paddingForAlignment(4, this->height), newMipCount, this->frameCount, this->getFaceCount(), this->sliceCount, filter);
906 } else {
907 this->regenerateImageData(newFormat, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->sliceCount, filter);
908 }
909}
910
911uint8_t VTF::getMipCount() const {
912 return this->mipCount;
913}
914
915bool VTF::setMipCount(uint8_t newMipCount) {
916 if (!this->hasImageData()) {
917 return false;
918 }
919 if (this->platform != PLATFORM_PC && newMipCount > 1) {
920 newMipCount = ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height);
921 } else if (const auto recommended = ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height); newMipCount > recommended) {
922 newMipCount = recommended;
923 if (newMipCount == 1) {
924 return false;
925 }
926 }
927 if (newMipCount > 1) {
928 this->flags &= ~(FLAG_NO_MIP | FLAG_NO_LOD);
929 } else {
930 this->flags |= FLAG_NO_MIP | FLAG_NO_LOD;
931 }
932 this->regenerateImageData(this->format, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->sliceCount);
933 return true;
934}
935
937 if (this->platform == VTF::PLATFORM_PC) {
938 return this->setMipCount(ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height));
939 }
940 return this->setMipCount(ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height));
941}
942
944 auto* imageResource = this->getResourceInternal(Resource::TYPE_IMAGE_DATA);
945 if (!imageResource || !this->hasImageData()) {
946 return;
947 }
948
949 if (this->mipCount <= 1) {
950 if (!this->setRecommendedMipCount() || this->mipCount <= 1) {
951 return;
952 }
953 }
954
955 auto* outputDataPtr = imageResource->data.data();
956 const auto faceCount = this->getFaceCount();
957
958#ifdef SOURCEPP_BUILD_WITH_THREADS
959 std::vector<std::future<void>> futures;
960 futures.reserve((this->mipCount - 1) * this->frameCount * faceCount * this->sliceCount);
961#endif
962 for (int j = 0; j < this->frameCount; j++) {
963 for (int k = 0; k < faceCount; k++) {
964 for (int l = 0; l < this->sliceCount; l++) {
965#ifdef SOURCEPP_BUILD_WITH_THREADS
966 futures.push_back(std::async(std::launch::async, [this, filter, outputDataPtr, faceCount, j, k, l] {
967#endif
968 for (int i = 1; i < this->mipCount; i++) {
969 auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(i - 1, j, k, l), this->format, ImageDimensions::getMipDim(i - 1, this->width), ImageDimensions::getMipDim(i, this->width), ImageDimensions::getMipDim(i - 1, this->height), ImageDimensions::getMipDim(i, this->height), this->isSRGB(), filter);
970 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, l, this->sliceCount) && mip.size() == length) {
971 std::memcpy(outputDataPtr + offset, mip.data(), length);
972 }
973 }
974#ifdef SOURCEPP_BUILD_WITH_THREADS
975 }));
976 if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) {
977 for (auto& future : futures) {
978 future.get();
979 }
980 futures.clear();
981 }
982#endif
983 }
984 }
985 }
986#ifdef SOURCEPP_BUILD_WITH_THREADS
987 for (auto& future : futures) {
988 future.get();
989 }
990#endif
991}
992
993uint16_t VTF::getFrameCount() const {
994 return this->frameCount;
995}
996
997bool VTF::setFrameCount(uint16_t newFrameCount) {
998 if (!this->hasImageData()) {
999 return false;
1000 }
1001 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, this->getFaceCount(), this->sliceCount);
1002 return true;
1003}
1004
1005uint8_t VTF::getFaceCount() const {
1006 if (!this->hasImageData()) {
1007 return 0;
1008 }
1009 const auto* image = this->getResource(Resource::TYPE_IMAGE_DATA);
1010 if (!image) {
1011 return 0;
1012 }
1013 if (!(this->flags & VTF::FLAG_ENVMAP)) {
1014 return 1;
1015 }
1016 if (this->version >= 6) {
1017 // All v7.6 VTFs are sane, and we need this special case to fix a bug in the parser where
1018 // it won't recognize cubemaps as cubemaps because the image resource is compressed!
1019 return 6;
1020 }
1021 const auto expectedLength = ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, 6, this->width, this->height, this->sliceCount);
1022 if (this->version >= 1 && this->version <= 4 && expectedLength < image->data.size()) {
1023 return 7;
1024 }
1025 if (expectedLength == image->data.size()) {
1026 return 6;
1027 }
1028 return 1;
1029}
1030
1031bool VTF::setFaceCount(bool isCubemap) {
1032 if (!this->hasImageData()) {
1033 return false;
1034 }
1035 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, isCubemap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, this->sliceCount);
1036 return true;
1037}
1038
1039uint16_t VTF::getSliceCount() const {
1040 return this->sliceCount;
1041}
1042
1043bool VTF::setSliceCount(uint16_t newSliceCount) {
1044 if (!this->hasImageData()) {
1045 return false;
1046 }
1047 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), newSliceCount);
1048 return true;
1049}
1050
1051bool VTF::setFrameFaceAndSliceCount(uint16_t newFrameCount, bool isCubemap, uint16_t newSliceCount) {
1052 if (!this->hasImageData()) {
1053 return false;
1054 }
1055 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, isCubemap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, newSliceCount);
1056 return true;
1057}
1058
1059uint16_t VTF::getStartFrame() const {
1060 return this->startFrame;
1061}
1062
1063void VTF::setStartFrame(uint16_t newStartFrame) {
1064 this->startFrame = newStartFrame;
1065}
1066
1067math::Vec3f VTF::getReflectivity() const {
1068 return this->reflectivity;
1069}
1070
1071void VTF::setReflectivity(sourcepp::math::Vec3f newReflectivity) {
1072 this->reflectivity = newReflectivity;
1073}
1074
1076 static constexpr auto getReflectivityForImage = [](const VTF& vtf, uint16_t frame, uint8_t face, uint16_t slice) {
1077 static constexpr auto getReflectivityForPixel = [](const ImagePixel::RGBA8888* pixel) -> math::Vec3f {
1078 // http://markjstock.org/doc/gsd_talk_11_notes.pdf page 11
1079 math::Vec3f ref{static_cast<float>(pixel->r), static_cast<float>(pixel->g), static_cast<float>(pixel->b)};
1080 ref /= 255.f * 0.9f;
1081 ref[0] *= ref[0];
1082 ref[1] *= ref[1];
1083 ref[2] *= ref[2];
1084 return ref;
1085 };
1086
1087 auto rgba8888Data = vtf.getImageDataAsRGBA8888(0, frame, face, slice);
1088 math::Vec3f out{};
1089 for (uint64_t i = 0; i < rgba8888Data.size(); i += 4) {
1090 out += getReflectivityForPixel(reinterpret_cast<ImagePixel::RGBA8888*>(rgba8888Data.data() + i));
1091 }
1092 return out / (rgba8888Data.size() / sizeof(ImagePixel::RGBA8888));
1093 };
1094
1095 const auto faceCount = this->getFaceCount();
1096
1097#ifdef SOURCEPP_BUILD_WITH_THREADS
1098 if (this->frameCount > 1 || faceCount > 1 || this->sliceCount > 1) {
1099 std::vector<std::future<math::Vec3f>> futures;
1100 futures.reserve(this->frameCount * faceCount * this->sliceCount);
1101
1102 this->reflectivity = {};
1103 for (int j = 0; j < this->frameCount; j++) {
1104 for (int k = 0; k < faceCount; k++) {
1105 for (int l = 0; l < this->sliceCount; l++) {
1106 futures.push_back(std::async(std::launch::async, [this, j, k, l] {
1107 return getReflectivityForImage(*this, j, k, l);
1108 }));
1109 if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) {
1110 for (auto& future : futures) {
1111 this->reflectivity += future.get();
1112 }
1113 futures.clear();
1114 }
1115 }
1116 }
1117 }
1118
1119 for (auto& future : futures) {
1120 this->reflectivity += future.get();
1121 }
1122 this->reflectivity /= this->frameCount * faceCount * this->sliceCount;
1123 } else {
1124 this->reflectivity = getReflectivityForImage(*this, 0, 0, 0);
1125 }
1126#else
1127 this->reflectivity = {};
1128 for (int j = 0; j < this->frameCount; j++) {
1129 for (int k = 0; k < faceCount; k++) {
1130 for (int l = 0; l < this->sliceCount; l++) {
1131 this->reflectivity += getReflectivityForImage(*this, j, k, l);
1132 }
1133 }
1134 }
1135 this->reflectivity /= this->frameCount * faceCount * this->sliceCount;
1136#endif
1137}
1138
1140 return this->bumpMapScale;
1141}
1142
1143void VTF::setBumpMapScale(float newBumpMapScale) {
1144 this->bumpMapScale = newBumpMapScale;
1145}
1146
1148 return this->thumbnailFormat;
1149}
1150
1151uint8_t VTF::getThumbnailWidth() const {
1152 return this->thumbnailWidth;
1153}
1154
1156 return this->thumbnailHeight;
1157}
1158
1159const std::vector<Resource>& VTF::getResources() const {
1160 return this->resources;
1161}
1162
1164 for (const auto& resource : this->resources) {
1165 if (resource.type == type) {
1166 return &resource;
1167 }
1168 }
1169 return nullptr;
1170}
1171
1173 for (auto& resource : this->resources) {
1174 if (resource.type == type) {
1175 return &resource;
1176 }
1177 }
1178 return nullptr;
1179}
1180
1181void VTF::setResourceInternal(Resource::Type type, std::span<const std::byte> data_) {
1182 if (const auto* resource = this->getResource(type); resource && resource->data.size() == data_.size()) {
1183 std::memcpy(resource->data.data(), data_.data(), data_.size());
1184 return;
1185 }
1186
1187 // Store resource data
1188 std::unordered_map<Resource::Type, std::pair<std::vector<std::byte>, uint64_t>> resourceData;
1189 for (const auto& [type_, flags_, dataSpan] : this->resources) {
1190 resourceData[type_] = {std::vector<std::byte>{dataSpan.begin(), dataSpan.end()}, 0};
1191 }
1192
1193 // Set new resource
1194 if (data_.empty()) {
1195 resourceData.erase(type);
1196 } else {
1197 resourceData[type] = {{data_.begin(), data_.end()}, 0};
1198 }
1199
1200 // Save the data
1201 this->data.clear();
1202 BufferStream writer{this->data};
1203
1204 for (auto resourceType : Resource::getOrder()) {
1205 if (!resourceData.contains(resourceType)) {
1206 continue;
1207 }
1208 auto& [specificResourceData, offset] = resourceData[resourceType];
1209 if (resourceType == type) {
1210 Resource newResource{
1211 type,
1212 specificResourceData.size() <= sizeof(uint32_t) ? Resource::FLAG_LOCAL_DATA : Resource::FLAG_NONE,
1213 {this->data.data() + offset, specificResourceData.size()},
1214 };
1215 if (auto* resourcePtr = this->getResourceInternal(type)) {
1216 *resourcePtr = newResource;
1217 } else {
1218 this->resources.push_back(newResource);
1219 }
1220 } else if (!resourceData.contains(resourceType)) {
1221 continue;
1222 }
1223 offset = writer.tell();
1224 writer.write(specificResourceData);
1225 }
1226 this->data.resize(writer.size());
1227
1228 for (auto& [type_, flags_, dataSpan] : this->resources) {
1229 if (resourceData.contains(type_)) {
1230 const auto& [specificResourceData, offset] = resourceData[type_];
1231 dataSpan = {this->data.data() + offset, specificResourceData.size()};
1232 }
1233 }
1234}
1235
1237 std::erase_if(this->resources, [type](const Resource& resource) { return resource.type == type; });
1238}
1239
1240void VTF::regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t newHeight, uint8_t newMipCount, uint16_t newFrameCount, uint8_t newFaceCount, uint16_t newSliceCount, ImageConversion::ResizeFilter filter) {
1241 if (!newWidth) { newWidth = 1; }
1242 if (!newHeight) { newHeight = 1; }
1243 if (!newMipCount) { newMipCount = 1; }
1244 if (!newFrameCount) { newFrameCount = 1; }
1245 if (!newFaceCount) { newFaceCount = 1; }
1246 if (!newSliceCount) { newSliceCount = 1; }
1247
1248 const auto faceCount = this->getFaceCount();
1249 if (this->format == newFormat && this->width == newWidth && this->height == newHeight && this->mipCount == newMipCount && this->frameCount == newFrameCount && faceCount == newFaceCount && this->sliceCount == newSliceCount) {
1250 return;
1251 }
1252
1253 std::vector<std::byte> newImageData;
1254 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
1255 if (this->format != newFormat && this->width == newWidth && this->height == newHeight && this->mipCount == newMipCount && this->frameCount == newFrameCount && faceCount == newFaceCount && this->sliceCount == newSliceCount) {
1256 newImageData = ImageConversion::convertSeveralImageDataToFormat(imageResource->data, this->format, newFormat, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->sliceCount);
1257 } else {
1258 newImageData.resize(ImageFormatDetails::getDataLength(this->format, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newSliceCount));
1259 for (int i = newMipCount - 1; i >= 0; i--) {
1260 for (int j = 0; j < newFrameCount; j++) {
1261 for (int k = 0; k < newFaceCount; k++) {
1262 for (int l = 0; l < newSliceCount; l++) {
1263 if (i < this->mipCount && j < this->frameCount && k < faceCount && l < this->sliceCount) {
1264 auto imageSpan = this->getImageDataRaw(i, j, k, l);
1265 std::vector<std::byte> image{imageSpan.begin(), imageSpan.end()};
1266 if (this->width != newWidth || this->height != newHeight) {
1267 image = ImageConversion::resizeImageData(image, this->format, ImageDimensions::getMipDim(i, this->width), ImageDimensions::getMipDim(i, newWidth), ImageDimensions::getMipDim(i, this->height), ImageDimensions::getMipDim(i, newHeight), this->isSRGB(), filter);
1268 }
1269 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, newMipCount, j, newFrameCount, k, newFaceCount, newWidth, newHeight, l, newSliceCount) && image.size() == length) {
1270 std::memcpy(newImageData.data() + offset, image.data(), length);
1271 }
1272 }
1273 }
1274 }
1275 }
1276 }
1277 if (this->format != newFormat) {
1278 newImageData = ImageConversion::convertSeveralImageDataToFormat(newImageData, this->format, newFormat, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newSliceCount);
1279 }
1280 }
1281 } else {
1282 newImageData.resize(ImageFormatDetails::getDataLength(newFormat, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newSliceCount));
1283 }
1284
1285 this->format = newFormat;
1286 this->width = newWidth;
1287 this->height = newHeight;
1288 this->mipCount = newMipCount;
1289 this->frameCount = newFrameCount;
1290 if (newFaceCount > 1) {
1291 this->flags |= FLAG_ENVMAP;
1292 } else {
1293 this->flags &= ~FLAG_ENVMAP;
1294 }
1295 this->sliceCount = newSliceCount;
1296
1298}
1299
1300std::vector<std::byte> VTF::getParticleSheetFrameDataRaw(uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1301 spriteWidth = 0;
1302 spriteHeight = 0;
1303
1304 auto shtResource = this->getResource(Resource::TYPE_PARTICLE_SHEET_DATA);
1305 if (!shtResource) {
1306 return {};
1307 }
1308
1309 auto sht = shtResource->getDataAsParticleSheet();
1310 const auto* sequence = sht.getSequenceFromID(shtSequenceID);
1311 if (!sequence || sequence->frames.size() <= shtFrame || shtBounds >= sht.getFrameBoundsCount()) {
1312 return {};
1313 }
1314
1315 // These values are likely slightly too large thanks to float magic, use a
1316 // shader that scales UVs instead of this function if precision is a concern
1317 // This will also break if any of the bounds are above 1 or below 0, but that
1318 // hasn't been observed in official textures
1319 const auto& bounds = sequence->frames[shtFrame].bounds[shtBounds];
1320 uint16_t x1 = std::clamp<uint16_t>(std::floor(bounds.x1 * static_cast<float>(this->getWidth(mip))), 0, this->getWidth(mip));
1321 uint16_t y1 = std::clamp<uint16_t>(std::ceil( bounds.y1 * static_cast<float>(this->getHeight(mip))), 0, this->getHeight(mip));
1322 uint16_t x2 = std::clamp<uint16_t>(std::ceil( bounds.x2 * static_cast<float>(this->getWidth(mip))), 0, this->getHeight(mip));
1323 uint16_t y2 = std::clamp<uint16_t>(std::floor(bounds.y2 * static_cast<float>(this->getHeight(mip))), 0, this->getWidth(mip));
1324
1325 if (x1 > x2) [[unlikely]] {
1326 std::swap(x1, x2);
1327 }
1328 if (y1 > y2) [[unlikely]] {
1329 std::swap(y1, y2);
1330 }
1331 spriteWidth = x2 - x1;
1332 spriteWidth = y2 - y1;
1333
1334 const auto out = ImageConversion::cropImageData(this->getImageDataRaw(mip, frame, face, slice), this->getFormat(), this->getWidth(mip), spriteWidth, x1, this->getHeight(mip), spriteHeight, y1);
1335 if (out.empty()) {
1336 spriteWidth = 0;
1337 spriteHeight = 0;
1338 }
1339 return out;
1340}
1341
1342std::vector<std::byte> VTF::getParticleSheetFrameDataAs(ImageFormat newFormat, uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1343 return ImageConversion::convertImageDataToFormat(this->getParticleSheetFrameDataRaw(spriteWidth, spriteHeight, shtSequenceID, shtFrame, shtBounds, mip, frame, face, slice), this->getFormat(), newFormat, spriteWidth, spriteHeight);
1344}
1345
1346std::vector<std::byte> VTF::getParticleSheetFrameDataAsRGBA8888(uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1347 return this->getParticleSheetFrameDataAs(ImageFormat::RGBA8888, spriteWidth, spriteHeight, shtSequenceID, shtFrame, shtBounds, mip, frame, face, slice);
1348}
1349
1351 std::vector<std::byte> particleSheetData;
1352 BufferStream writer{particleSheetData};
1353
1354 const auto bakedSheet = value.bake();
1355 writer.write<uint32_t>(bakedSheet.size()).write(bakedSheet);
1356 particleSheetData.resize(writer.size());
1357
1359}
1360
1363}
1364
1365void VTF::setCRCResource(uint32_t value) {
1366 this->setResourceInternal(Resource::TYPE_CRC, {reinterpret_cast<const std::byte*>(&value), sizeof(value)});
1367}
1368
1371}
1372
1373void VTF::setLODResource(uint8_t u, uint8_t v, uint8_t u360, uint8_t v360) {
1374 uint32_t lodData;
1375 BufferStream writer{&lodData, sizeof(lodData)};
1376
1377 writer << u << v << u360 << v360;
1378
1379 this->setResourceInternal(Resource::TYPE_LOD_CONTROL_INFO, {reinterpret_cast<const std::byte*>(&lodData), sizeof(lodData)});
1380}
1381
1384}
1385
1386void VTF::setExtendedFlagsResource(uint32_t value) {
1387 this->setResourceInternal(Resource::TYPE_EXTENDED_FLAGS, {reinterpret_cast<const std::byte*>(&value), sizeof(value)});
1388}
1389
1392}
1393
1394void VTF::setKeyValuesDataResource(const std::string& value) {
1395 std::vector<std::byte> keyValuesData;
1396 BufferStream writer{keyValuesData};
1397
1398 writer.write<uint32_t>(value.size()).write(value, false);
1399 keyValuesData.resize(writer.size());
1400
1402}
1403
1406}
1407
1409 std::vector<std::byte> hotspotData;
1410 BufferStream writer{hotspotData};
1411
1412 const auto bakedHotspotData = value.bake();
1413 writer.write<uint32_t>(bakedHotspotData.size()).write(bakedHotspotData);
1414 hotspotData.resize(writer.size());
1415
1417}
1418
1421}
1422
1424 return this->compressionLevel;
1425}
1426
1427void VTF::setCompressionLevel(int16_t newCompressionLevel) {
1428 this->compressionLevel = newCompressionLevel;
1429}
1430
1432 return this->compressionMethod;
1433}
1434
1436 if (newCompressionMethod == CompressionMethod::CONSOLE_LZMA && this->platform == VTF::PLATFORM_PC) {
1438 } else if (newCompressionMethod != CompressionMethod::CONSOLE_LZMA && this->platform != VTF::PLATFORM_PC) {
1440 } else {
1441 this->compressionMethod = newCompressionMethod;
1442 }
1443}
1444
1445bool VTF::hasImageData() const {
1446 return this->format != ImageFormat::EMPTY && this->width > 0 && this->height > 0;
1447}
1448
1449std::span<const std::byte> VTF::getImageDataRaw(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1450 if (const auto imageResource = this->getResource(Resource::TYPE_IMAGE_DATA)) {
1451 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, this->getFaceCount(), this->width, this->height, slice, this->sliceCount)) {
1452 return imageResource->data.subspan(offset, length);
1453 }
1454 }
1455 return {};
1456}
1457
1458std::vector<std::byte> VTF::getImageDataAs(ImageFormat newFormat, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1459 const auto rawImageData = this->getImageDataRaw(mip, frame, face, slice);
1460 if (rawImageData.empty()) {
1461 return {};
1462 }
1463 return ImageConversion::convertImageDataToFormat(rawImageData, this->format, newFormat, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height));
1464}
1465
1466std::vector<std::byte> VTF::getImageDataAsRGBA8888(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1467 return this->getImageDataAs(ImageFormat::RGBA8888, mip, frame, face, slice);
1468}
1469
1470bool VTF::setImage(std::span<const std::byte> imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) {
1471 if (imageData_.empty()) {
1472 return false;
1473 }
1474
1475 if (!this->hasImageData()) {
1476 uint16_t resizedWidth = width_, resizedHeight = height_;
1477 ImageConversion::setResizedDims(resizedWidth, this->imageWidthResizeMethod, resizedHeight, this->imageHeightResizeMethod);
1478 if (ImageFormatDetails::compressed(format_)) {
1479 resizedWidth += math::paddingForAlignment(4, resizedWidth);
1480 resizedHeight += math::paddingForAlignment(4, resizedHeight);
1481 }
1482 if (const auto newMipCount = ImageDimensions::getRecommendedMipCountForDims(format_, resizedWidth, resizedHeight); newMipCount <= mip) {
1483 mip = newMipCount - 1;
1484 }
1485 if (face > 6 || (face == 6 && (this->version < 1 || this->version > 4))) {
1486 return false;
1487 }
1488 this->regenerateImageData(format_, resizedWidth, resizedHeight, mip + 1, frame + 1, face ? (face < 5 ? 5 : face) : 0, slice + 1);
1489 }
1490
1491 const auto faceCount = this->getFaceCount();
1492 if (this->mipCount <= mip || this->frameCount <= frame || faceCount <= face || this->sliceCount <= slice) {
1493 return false;
1494 }
1495
1496 const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA);
1497 if (!imageResource) {
1498 return false;
1499 }
1500 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, faceCount, this->width, this->height, slice, this->sliceCount)) {
1501 std::vector<std::byte> image{imageData_.begin(), imageData_.end()};
1502 const auto newWidth = ImageDimensions::getMipDim(mip, this->width);
1503 const auto newHeight = ImageDimensions::getMipDim(mip, this->height);
1504 if (width_ != newWidth || height_ != newHeight) {
1505 image = ImageConversion::resizeImageData(image, format_, width_, newWidth, height_, newHeight, this->isSRGB(), filter);
1506 }
1507 if (format_ != this->format) {
1508 image = ImageConversion::convertImageDataToFormat(image, format_, this->format, newWidth, newHeight);
1509 }
1510 std::memcpy(imageResource->data.data() + offset, image.data(), image.size());
1511 }
1512 return true;
1513}
1514
1515bool VTF::setImage(const std::string& imagePath, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) {
1516 ImageFormat inputFormat;
1517 int inputWidth, inputHeight, inputFrameCount;
1518 auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount);
1519
1520 // Unable to decode file
1521 if (imageData_.empty() || inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) {
1522 return false;
1523 }
1524
1525 // One frame (normal)
1526 if (inputFrameCount == 1) {
1527 return this->setImage(imageData_, inputFormat, inputWidth, inputHeight, filter, mip, frame, face, slice);
1528 }
1529
1530 // Multiple frames (GIF)
1531 bool allSuccess = true;
1532 const auto frameSize = ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight);
1533 for (int currentFrame = 0; currentFrame < inputFrameCount; currentFrame++) {
1534 if (!this->setImage({imageData_.data() + currentFrame * frameSize, imageData_.data() + currentFrame * frameSize + frameSize}, inputFormat, inputWidth, inputHeight, filter, mip, frame + currentFrame, face, slice)) {
1535 allSuccess = false;
1536 }
1537 if (currentFrame == 0 && this->frameCount < frame + inputFrameCount) {
1538 // Call this after setting the first image, this function is a no-op if no image data is present yet
1539 this->setFrameCount(frame + inputFrameCount);
1540 }
1541 }
1542 return allSuccess;
1543}
1544
1545std::vector<std::byte> VTF::saveImageToFile(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const {
1546 return ImageConversion::convertImageDataToFile(this->getImageDataRaw(mip, frame, face, slice), this->format, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height), fileFormat);
1547}
1548
1549bool VTF::saveImageToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const {
1550 if (auto data_ = this->saveImageToFile(mip, frame, face, slice, fileFormat); !data_.empty()) {
1551 return fs::writeFileBuffer(imagePath, data_);
1552 }
1553 return false;
1554}
1555
1557 return this->thumbnailFormat != ImageFormat::EMPTY && this->thumbnailWidth > 0 && this->thumbnailHeight > 0;
1558}
1559
1560std::span<const std::byte> VTF::getThumbnailDataRaw() const {
1561 if (const auto thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA)) {
1562 return thumbnailResource->data;
1563 }
1564 return {};
1565}
1566
1567std::vector<std::byte> VTF::getThumbnailDataAs(ImageFormat newFormat) const {
1568 const auto rawThumbnailData = this->getThumbnailDataRaw();
1569 if (rawThumbnailData.empty()) {
1570 return {};
1571 }
1572 return ImageConversion::convertImageDataToFormat(rawThumbnailData, this->thumbnailFormat, newFormat, this->thumbnailWidth, this->thumbnailHeight);
1573}
1574
1575std::vector<std::byte> VTF::getThumbnailDataAsRGBA8888() const {
1577}
1578
1579void VTF::setThumbnail(std::span<const std::byte> imageData_, ImageFormat format_, uint16_t width_, uint16_t height_) {
1580 if (format_ != this->thumbnailFormat) {
1582 } else {
1584 }
1585 this->thumbnailWidth = width_;
1586 this->thumbnailHeight = height_;
1587}
1588
1590 if (!this->hasImageData()) {
1591 return;
1592 }
1594 this->thumbnailWidth = 16;
1595 this->thumbnailHeight = 16;
1597}
1598
1601 this->thumbnailWidth = 0;
1602 this->thumbnailHeight = 0;
1604}
1605
1606std::vector<std::byte> VTF::saveThumbnailToFile(ImageConversion::FileFormat fileFormat) const {
1608}
1609
1610bool VTF::saveThumbnailToFile(const std::string& imagePath, ImageConversion::FileFormat fileFormat) const {
1611 if (auto data_ = this->saveThumbnailToFile(fileFormat); !data_.empty()) {
1612 return fs::writeFileBuffer(imagePath, data_);
1613 }
1614 return false;
1615}
1616
1617std::vector<std::byte> VTF::bake() const {
1618 std::vector<std::byte> out;
1619 BufferStream writer{out};
1620
1621 static constexpr auto writeNonLocalResource = [](BufferStream& writer_, Resource::Type type, std::span<const std::byte> data, VTF::Platform platform) {
1622 if (platform != VTF::PLATFORM_PC) {
1623 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(&type));
1624 }
1625 writer_.write<uint32_t>(type);
1626 const auto resourceOffsetPos = writer_.tell();
1627 writer_.seek(0, std::ios::end);
1628 const auto resourceOffsetValue = writer_.tell();
1629 writer_.write(data);
1630 writer_.seek_u(resourceOffsetPos).write<uint32_t>(resourceOffsetValue);
1631 };
1632
1633 switch (this->platform) {
1634 case PLATFORM_UNKNOWN:
1635 break;
1636 case PLATFORM_PC: {
1637 writer
1638 .write(VTF_SIGNATURE)
1639 .write<int32_t>(7)
1640 .write(this->version);
1641
1642 const auto headerLengthPos = writer.tell();
1643 writer.write<uint32_t>(0);
1644
1645 writer
1646 .write(this->width)
1647 .write(this->height)
1648 .write(this->flags)
1649 .write(this->frameCount)
1650 .write(this->startFrame)
1651 .write<uint32_t>(0) // padding
1652 .write(this->reflectivity)
1653 .write<uint32_t>(0) // padding
1654 .write(this->bumpMapScale)
1655 .write(this->format)
1656 .write(this->mipCount)
1657 .write(ImageFormat::DXT1)
1658 .write(this->thumbnailWidth)
1659 .write(this->thumbnailHeight);
1660
1661 if (this->version >= 2) {
1662 writer << this->sliceCount;
1663 }
1664
1665 if (this->version < 3) {
1666 const auto headerAlignment = math::paddingForAlignment(16, writer.tell());
1667 for (uint16_t i = 0; i < headerAlignment; i++) {
1668 writer.write<std::byte>({});
1669 }
1670 const auto headerSize = writer.tell();
1671 writer.seek_u(headerLengthPos).write<uint32_t>(headerSize).seek_u(headerSize);
1672
1673 if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
1674 writer.write(thumbnailResource->data);
1675 }
1676 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
1677 writer.write(imageResource->data);
1678 }
1679 } else {
1680 std::vector<std::byte> auxCompressionResourceData;
1681 std::vector<std::byte> compressedImageResourceData;
1682 bool hasAuxCompression = false;
1683 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA)) {
1684 hasAuxCompression = this->version >= 6 && this->compressionLevel != 0;
1685 if (hasAuxCompression) {
1686 const auto faceCount = this->getFaceCount();
1687 auxCompressionResourceData.resize((this->mipCount * this->frameCount * faceCount + 2) * sizeof(uint32_t));
1688 BufferStream auxWriter{auxCompressionResourceData, false};
1689
1690 // Format of aux resource is as follows, with each item of unspecified type being a 4 byte integer:
1691 // - Size of resource in bytes, not counting this int
1692 // - Compression level, method (2 byte integers)
1693 // - (X times) Size of each mip-face-frame combo
1694 auxWriter
1695 .write<uint32_t>(auxCompressionResourceData.size() - sizeof(uint32_t))
1696 .write(this->compressionLevel)
1697 .write(this->compressionMethod);
1698
1699 for (int i = this->mipCount - 1; i >= 0; i--) {
1700 for (int j = 0; j < this->frameCount; j++) {
1701 for (int k = 0; k < faceCount; k++) {
1702 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->sliceCount)) {
1703 auto compressedData = ::compressData({imageResource->data.data() + offset, length * this->sliceCount}, this->compressionLevel, this->compressionMethod);
1704 compressedImageResourceData.insert(compressedImageResourceData.end(), compressedData.begin(), compressedData.end());
1705 auxWriter.write<uint32_t>(compressedData.size());
1706 }
1707 }
1708 }
1709 }
1710 }
1711 }
1712
1713 writer
1714 .write<uint8_t>(0) // padding
1715 .write<uint8_t>(0) // padding
1716 .write<uint8_t>(0) // padding
1717 .write<uint32_t>(this->getResources().size() + hasAuxCompression)
1718 .write<uint64_t>(0); // padding
1719
1720 const auto resourceStart = writer.tell();
1721 const auto headerSize = resourceStart + ((this->getResources().size() + hasAuxCompression) * sizeof(uint64_t));
1722 writer.seek_u(headerLengthPos).write<uint32_t>(headerSize).seek_u(resourceStart);
1723 while (writer.tell() < headerSize) {
1724 writer.write<uint64_t>(0);
1725 }
1726 writer.seek_u(resourceStart);
1727
1728 for (const auto resourceType : Resource::getOrder()) {
1729 if (hasAuxCompression && resourceType == Resource::TYPE_AUX_COMPRESSION) {
1730 writeNonLocalResource(writer, resourceType, auxCompressionResourceData, this->platform);
1731 } else if (hasAuxCompression && resourceType == Resource::TYPE_IMAGE_DATA) {
1732 writeNonLocalResource(writer, resourceType, compressedImageResourceData, this->platform);
1733 } else if (const auto* resource = this->getResource(resourceType)) {
1734 if ((resource->flags & Resource::FLAG_LOCAL_DATA) && resource->data.size() == sizeof(uint32_t)) {
1735 writer.write<uint32_t>((Resource::FLAG_LOCAL_DATA << 24) | resource->type);
1736 writer.write(resource->data);
1737 } else {
1738 writeNonLocalResource(writer, resource->type, resource->data, this->platform);
1739 }
1740 }
1741 }
1742 }
1743
1744 out.resize(writer.size());
1745 return out;
1746 }
1747 case PLATFORM_X360:
1749 case PLATFORM_PS3_PORTAL2: {
1750 if (this->platform == PLATFORM_PS3_PORTAL2) {
1751 writer << VTF3_SIGNATURE;
1752 writer.set_big_endian(true);
1753 writer << PLATFORM_PS3_ORANGEBOX; // Intentional
1754 } else {
1755 writer << VTFX_SIGNATURE;
1756 writer.set_big_endian(true);
1757 writer << this->platform;
1758 }
1759
1760 // Go down until top level texture is <1mb, matches makegamedata.exe output
1761 uint8_t mipSkip = 0;
1762 for (int mip = 0; mip < this->mipCount; mip++, mipSkip++) {
1763 if (ImageFormatDetails::getDataLength(this->format, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height), ImageDimensions::getMipDim(mip, this->sliceCount)) < 1024 * 1024) {
1764 break;
1765 }
1766 }
1767
1768 writer.write<uint32_t>(8);
1769 const auto headerLengthPos = writer.tell();
1770 writer
1771 .write<uint32_t>(0)
1772 .write(this->flags)
1773 .write(this->width)
1774 .write(this->height)
1775 .write(this->sliceCount)
1776 .write(this->frameCount);
1777 const auto preloadPos = writer.tell();
1778 writer
1779 .write<uint16_t>(0) // preload size
1780 .write(mipSkip)
1781 .write<uint8_t>(this->resources.size())
1782 .write(this->reflectivity[0])
1783 .write(this->reflectivity[1])
1784 .write(this->reflectivity[2])
1785 .write(this->bumpMapScale)
1786 .write(this->format)
1787 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[0] * 255)), 0, 255))
1788 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[1] * 255)), 0, 255))
1789 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[2] * 255)), 0, 255))
1790 .write<uint8_t>(255);
1791 const auto compressionPos = writer.tell();
1792 writer.write<uint32_t>(0); // compressed length
1793
1794 if (this->platform == PLATFORM_PS3_PORTAL2) {
1795 // 16 byte aligned
1796 writer.write<uint32_t>(0);
1797 }
1798
1799 // LZMA compression has not been observed on the PS3 copy of The Orange Box
1800 // todo(vtfpp): check cubemaps
1801 std::vector<std::byte> imageResourceData;
1802 bool hasCompression = false;
1803 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA)) {
1804 imageResourceData.assign(imageResource->data.begin(), imageResource->data.end());
1805 ::swapImageDataEndianForConsole<false>(imageResourceData, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->sliceCount, this->platform);
1806
1807 if (this->platform != PLATFORM_PS3_ORANGEBOX) {
1808 hasCompression = this->compressionMethod == CompressionMethod::CONSOLE_LZMA;
1809 if (hasCompression) {
1810 auto fixedCompressionLevel = this->compressionLevel;
1811 if (this->compressionLevel == 0) {
1812 // Compression level defaults to 0, so it works differently on console.
1813 // Rather than not compress on 0, 0 will be replaced with the default
1814 // compression level (6) if the compression method is LZMA.
1815 fixedCompressionLevel = 6;
1816 }
1817 if (const auto compressedData = compression::compressValveLZMA(imageResourceData, fixedCompressionLevel)) {
1818 imageResourceData.assign(compressedData->begin(), compressedData->end());
1819 } else {
1820 hasCompression = false;
1821 }
1822 }
1823 }
1824 }
1825
1826 const auto resourceStart = writer.tell();
1827 const auto headerSize = resourceStart + (this->getResources().size() * sizeof(uint64_t));
1828 writer.seek_u(headerLengthPos).write<uint32_t>(headerSize).seek_u(resourceStart);
1829 while (writer.tell() < headerSize) {
1830 writer.write<uint64_t>(0);
1831 }
1832 writer.seek_u(resourceStart);
1833
1834 for (const auto resourceType : Resource::getOrder()) {
1835 if (resourceType == Resource::TYPE_IMAGE_DATA) {
1836 auto curPos = writer.tell();
1837 const auto imagePos = writer.seek(0, std::ios::end).tell();
1838 writer.seek_u(preloadPos).write(std::max<uint16_t>(imagePos, 2048)).seek_u(curPos);
1839
1840 writeNonLocalResource(writer, resourceType, imageResourceData, this->platform);
1841
1842 if (hasCompression) {
1843 curPos = writer.tell();
1844 writer.seek_u(compressionPos).write<uint32_t>(imageResourceData.size()).seek_u(curPos);
1845 }
1846 } else if (const auto* resource = this->getResource(resourceType)) {
1847 std::vector<std::byte> resData{resource->data.begin(), resource->data.end()};
1848
1849 if (resource->type == Resource::TYPE_THUMBNAIL_DATA) {
1850 ::swapImageDataEndianForConsole<false>(resData, this->thumbnailFormat, 1, 1, 1, this->thumbnailWidth, this->thumbnailHeight, 1, this->platform);
1851 } else if (!(resource->flags & Resource::FLAG_LOCAL_DATA) && resData.size() >= sizeof(uint32_t)) {
1852 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(resData.data()));
1853 }
1854
1855 if ((resource->flags & Resource::FLAG_LOCAL_DATA) && resource->data.size() == sizeof(uint32_t)) {
1856 writer.set_big_endian(false);
1857 writer.write<uint32_t>((Resource::FLAG_LOCAL_DATA << 24) | resource->type);
1858 writer.set_big_endian(true);
1859 writer.write(resData);
1860 } else {
1861 writeNonLocalResource(writer, resource->type, resData, this->platform);
1862 }
1863 }
1864 }
1865
1866 out.resize(writer.size());
1867 return out;
1868 }
1869 }
1870 return {};
1871}
1872
1873bool VTF::bake(const std::string& vtfPath) const {
1874 return fs::writeFileBuffer(vtfPath, this->bake());
1875}
#define SOURCEPP_DEBUG_BREAK
Create a breakpoint in debug.
Definition: Macros.h:19
Definition: HOT.h:13
std::vector< std::byte > bake() const
Definition: HOT.cpp:61
Definition: SHT.h:13
std::vector< std::byte > bake() const
Definition: SHT.cpp:82
static constexpr uint32_t FLAGS_MASK_V5
Definition: VTF.h:203
uint16_t width
Definition: VTF.h:471
static constexpr uint32_t FLAGS_MASK_V2
Definition: VTF.h:173
Platform
Definition: VTF.h:216
@ PLATFORM_PC
Definition: VTF.h:218
@ PLATFORM_X360
Definition: VTF.h:219
@ PLATFORM_PS3_PORTAL2
Definition: VTF.h:221
@ PLATFORM_PS3_ORANGEBOX
Definition: VTF.h:220
@ PLATFORM_UNKNOWN
Definition: VTF.h:217
void setImageHeightResizeMethod(ImageConversion::ResizeMethod imageHeightResizeMethod_)
Definition: VTF.cpp:772
void removeKeyValuesDataResource()
Definition: VTF.cpp:1404
CompressionMethod compressionMethod
Definition: VTF.h:502
void computeReflectivity()
Definition: VTF.cpp:1075
uint8_t getThumbnailWidth() const
Definition: VTF.cpp:1151
std::vector< std::byte > saveThumbnailToFile(ImageConversion::FileFormat fileFormat=ImageConversion::FileFormat::DEFAULT) const
Definition: VTF.cpp:1606
ImageFormat getFormat() const
Definition: VTF.cpp:885
void setPlatform(Platform newPlatform)
Definition: VTF.cpp:682
uint16_t getHeight(uint8_t mip=0) const
Definition: VTF.cpp:780
VTF & operator=(const VTF &other)
Definition: VTF.cpp:527
int16_t compressionLevel
Definition: VTF.h:501
sourcepp::math::Vec3f reflectivity
Definition: VTF.h:479
uint16_t getWidth(uint8_t mip=0) const
Definition: VTF.cpp:776
uint16_t startFrame
Definition: VTF.h:476
static constexpr uint32_t FLAGS_MASK_INTERNAL
Definition: VTF.h:214
uint8_t mipCount
Definition: VTF.h:484
bool setRecommendedMipCount()
Definition: VTF.cpp:936
static bool createInternal(VTF &writer, CreationOptions options)
Definition: VTF.cpp:566
std::vector< std::byte > getParticleSheetFrameDataAsRGBA8888(uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition: VTF.cpp:1346
@ FLAG_NO_LOD
Definition: VTF.h:156
@ FLAG_MULTI_BIT_ALPHA
Definition: VTF.h:160
@ FLAG_ONE_BIT_ALPHA
Definition: VTF.h:159
@ FLAG_ENVMAP
Definition: VTF.h:161
@ FLAG_NO_MIP
Definition: VTF.h:155
void computeMips(ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition: VTF.cpp:943
void setImageWidthResizeMethod(ImageConversion::ResizeMethod imageWidthResizeMethod_)
Definition: VTF.cpp:768
uint8_t thumbnailWidth
Definition: VTF.h:487
void setCompressionLevel(int16_t newCompressionLevel)
Definition: VTF.cpp:1427
ImageConversion::ResizeMethod imageHeightResizeMethod
Definition: VTF.h:504
void setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMethod_, ImageConversion::ResizeMethod imageHeightResizeMethod_)
Definition: VTF.cpp:763
std::vector< std::byte > getParticleSheetFrameDataAs(ImageFormat newFormat, uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition: VTF.cpp:1342
static constexpr uint32_t FLAGS_MASK_V3
Definition: VTF.h:181
float getBumpMapScale() const
Definition: VTF.cpp:1139
void computeTransparencyFlags()
Definition: VTF.cpp:855
static constexpr uint32_t FLAGS_MASK_V4_TF2
Definition: VTF.h:195
std::vector< std::byte > data
Definition: VTF.h:465
bool setFrameFaceAndSliceCount(uint16_t newFrameCount, bool isCubeMap, uint16_t newSliceCount=1)
Definition: VTF.cpp:1051
ImageFormat format
Definition: VTF.h:483
uint16_t sliceCount
Definition: VTF.h:491
Platform getPlatform() const
Definition: VTF.cpp:678
void addFlags(uint32_t flags_)
Definition: VTF.cpp:827
uint8_t getFaceCount() const
Definition: VTF.cpp:1005
ImageFormat thumbnailFormat
Definition: VTF.h:486
void setFlags(uint32_t flags_)
Definition: VTF.cpp:823
void setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter)
Definition: VTF.cpp:784
void removeLODResource()
Definition: VTF.cpp:1382
ImageConversion::ResizeMethod getImageHeightResizeMethod() const
Definition: VTF.cpp:759
void setBumpMapScale(float newBumpMapScale)
Definition: VTF.cpp:1143
ImageFormat getThumbnailFormat() const
Definition: VTF.cpp:1147
uint16_t getStartFrame() const
Definition: VTF.cpp:1059
void setThumbnail(std::span< const std::byte > imageData_, ImageFormat format_, uint16_t width_, uint16_t height_)
Definition: VTF.cpp:1579
static constexpr auto FORMAT_DEFAULT
This value is only valid when passed to VTF::create through CreationOptions or VTF::setFormat.
Definition: VTF.h:251
bool hasThumbnailData() const
Definition: VTF.cpp:1556
void setReflectivity(sourcepp::math::Vec3f newReflectivity)
Definition: VTF.cpp:1071
void setSRGB(bool srgb)
Definition: VTF.cpp:839
const std::vector< Resource > & getResources() const
Definition: VTF.cpp:1159
void removeHotspotDataResource()
Definition: VTF.cpp:1419
@ FLAG_V4_SRGB
Definition: VTF.h:184
void setVersion(uint32_t newVersion)
Definition: VTF.cpp:722
std::vector< std::byte > saveImageToFile(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0, ImageConversion::FileFormat fileFormat=ImageConversion::FileFormat::DEFAULT) const
Definition: VTF.cpp:1545
Resource * getResourceInternal(Resource::Type type)
Definition: VTF.cpp:1172
std::vector< std::byte > bake() const
Definition: VTF.cpp:1617
bool setImage(std::span< const std::byte > imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0)
Definition: VTF.cpp:1470
bool hasImageData() const
Definition: VTF.cpp:1445
ImageConversion::ResizeMethod imageWidthResizeMethod
Definition: VTF.h:503
uint16_t getSliceCount() const
Definition: VTF.cpp:1039
void removeParticleSheetResource()
Definition: VTF.cpp:1361
std::vector< std::byte > getImageDataAsRGBA8888(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition: VTF.cpp:1466
uint16_t getFrameCount() const
Definition: VTF.cpp:993
uint8_t getMipCount() const
Definition: VTF.cpp:911
void setLODResource(uint8_t u, uint8_t v, uint8_t u360=0, uint8_t v360=0)
Definition: VTF.cpp:1373
void removeResourceInternal(Resource::Type type)
Definition: VTF.cpp:1236
bool setFrameCount(uint16_t newFrameCount)
Definition: VTF.cpp:997
ImageConversion::ResizeMethod getImageWidthResizeMethod() const
Definition: VTF.cpp:755
std::vector< std::byte > getImageDataAs(ImageFormat newFormat, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition: VTF.cpp:1458
void setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition: VTF.cpp:889
static ImageFormat getDefaultCompressedFormat(ImageFormat inputFormat, uint32_t version, bool isCubeMap)
Definition: VTF.cpp:870
void setStartFrame(uint16_t newStartFrame)
Definition: VTF.cpp:1063
std::vector< std::byte > getThumbnailDataAs(ImageFormat newFormat) const
Definition: VTF.cpp:1567
static constexpr uint32_t FLAGS_MASK_V5_CSGO
Definition: VTF.h:212
@ FLAG_V5_SRGB
Definition: VTF.h:199
@ FLAG_V5_PWL_CORRECTED
Definition: VTF.h:198
bool setSliceCount(uint16_t newSliceCount)
Definition: VTF.cpp:1043
std::span< const std::byte > getImageDataRaw(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition: VTF.cpp:1449
sourcepp::math::Vec3f getReflectivity() const
Definition: VTF.cpp:1067
std::vector< std::byte > getThumbnailDataAsRGBA8888() const
Definition: VTF.cpp:1575
bool isSRGB() const
Definition: VTF.cpp:835
uint32_t flags
Definition: VTF.h:473
void removeThumbnail()
Definition: VTF.cpp:1599
static constexpr auto FORMAT_UNCHANGED
This value is only valid when passed to VTF::create through CreationOptions.
Definition: VTF.h:248
uint32_t version
Definition: VTF.h:468
std::span< const std::byte > getThumbnailDataRaw() const
Definition: VTF.cpp:1560
bool setMipCount(uint8_t newMipCount)
Definition: VTF.cpp:915
void setExtendedFlagsResource(uint32_t value)
Definition: VTF.cpp:1386
CompressionMethod getCompressionMethod() const
Definition: VTF.cpp:1431
void setCRCResource(uint32_t value)
Definition: VTF.cpp:1365
void setCompressionMethod(CompressionMethod newCompressionMethod)
Definition: VTF.cpp:1435
void computeThumbnail(ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition: VTF.cpp:1589
bool setFaceCount(bool isCubeMap)
Definition: VTF.cpp:1031
uint8_t getThumbnailHeight() const
Definition: VTF.cpp:1155
const Resource * getResource(Resource::Type type) const
Definition: VTF.cpp:1163
static constexpr uint32_t FLAGS_MASK_V4
Definition: VTF.h:186
uint32_t getFlags() const
Definition: VTF.cpp:819
void setParticleSheetResource(const SHT &value)
Definition: VTF.cpp:1350
void setKeyValuesDataResource(const std::string &value)
Definition: VTF.cpp:1394
uint16_t height
Definition: VTF.h:472
void removeCRCResource()
Definition: VTF.cpp:1369
uint16_t frameCount
Definition: VTF.h:475
uint32_t getVersion() const
Definition: VTF.cpp:718
static bool create(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, const std::string &vtfPath, CreationOptions options)
Definition: VTF.cpp:618
int16_t getCompressionLevel() const
Definition: VTF.cpp:1423
void setHotspotDataResource(const HOT &value)
Definition: VTF.cpp:1408
void removeFlags(uint32_t flags_)
Definition: VTF.cpp:831
void setResourceInternal(Resource::Type type, std::span< const std::byte > data_)
Definition: VTF.cpp:1181
std::vector< Resource > resources
Definition: VTF.h:496
uint8_t thumbnailHeight
Definition: VTF.h:488
Platform platform
Definition: VTF.h:500
std::vector< std::byte > getParticleSheetFrameDataRaw(uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition: VTF.cpp:1300
void removeExtendedFlagsResource()
Definition: VTF.cpp:1390
void regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t newHeight, uint8_t newMipCount, uint16_t newFrameCount, uint8_t newFaceCount, uint16_t newSliceCount, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition: VTF.cpp:1240
bool opened
Definition: VTF.h:463
float bumpMapScale
Definition: VTF.h:482
std::optional< std::vector< std::byte > > compressValveLZMA(std::span< const std::byte > data, uint8_t compressLevel=6)
Definition: LZMA.cpp:8
constexpr auto VALVE_LZMA_SIGNATURE
Definition: LZMA.h:13
std::optional< std::vector< std::byte > > decompressValveLZMA(std::span< const std::byte > data)
Definition: LZMA.cpp:51
std::vector< std::byte > readFileBuffer(const std::string &filepath, std::size_t startOffset=0)
Definition: FS.cpp:9
bool writeFileBuffer(const std::string &filepath, std::span< const std::byte > buffer)
Definition: FS.cpp:27
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
Definition: Math.h:61
constexpr uint32_t log2ceil(uint32_t value)
Definition: Math.h:57
Definition: LZMA.h:11
std::vector< std::byte > convertFileToImageData(std::span< const std::byte > fileData, ImageFormat &format, int &width, int &height, int &frameCount)
std::vector< std::byte > convertImageDataToFile(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, FileFormat fileFormat=FileFormat::DEFAULT)
Converts image data to the given file format (PNG or EXR by default).
void setResizedDims(uint16_t &width, ResizeMethod widthResize, uint16_t &height, ResizeMethod heightResize)
Set the new image dimensions given a resize method.
std::vector< std::byte > gammaCorrectImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, float gamma)
Perform gamma correction on the given image data. Will not perform gamma correction if the input imag...
std::vector< std::byte > cropImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t xOffset, uint16_t height, uint16_t newHeight, uint16_t yOffset)
Crops the given image to the new dimensions. If the image format is compressed it will be converted t...
std::vector< std::byte > convertSeveralImageDataToFormat(std::span< const std::byte > imageData, ImageFormat oldFormat, ImageFormat newFormat, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount)
Converts several images from one format to another.
std::vector< std::byte > invertGreenChannelForImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height)
Invert the green channel. Meant for converting normal maps between OpenGL and DirectX formats.
std::vector< std::byte > convertImageDataToFormat(std::span< const std::byte > imageData, ImageFormat oldFormat, ImageFormat newFormat, uint16_t width, uint16_t height)
Converts an image from one format to another.
std::vector< std::byte > resizeImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t height, uint16_t newHeight, bool srgb, ResizeFilter filter, ResizeEdge edge=ResizeEdge::CLAMP)
Resize given image data to the new dimensions.
constexpr uint32_t getMipDim(uint8_t mip, uint16_t dim)
Definition: ImageFormats.h:650
constexpr uint8_t getActualMipCountForDimsOnConsole(uint16_t width, uint16_t height)
Definition: ImageFormats.h:685
constexpr uint8_t getRecommendedMipCountForDims(ImageFormat format, uint16_t width, uint16_t height)
Definition: ImageFormats.h:657
constexpr bool large(ImageFormat format)
Definition: ImageFormats.h:571
constexpr uint32_t getDataLength(ImageFormat format, uint16_t width, uint16_t height, uint16_t sliceCount=1)
Definition: ImageFormats.h:711
constexpr bool transparent(ImageFormat format)
Definition: ImageFormats.h:583
constexpr int8_t decompressedAlpha(ImageFormat format)
Definition: ImageFormats.h:414
constexpr bool getDataPosition(uint32_t &offset, uint32_t &length, ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice=0, uint16_t sliceCount=1)
Definition: ImageFormats.h:737
constexpr uint8_t bpp(ImageFormat format)
Definition: ImageFormats.h:436
constexpr bool compressed(ImageFormat format)
Definition: ImageFormats.h:579
Definition: HOT.h:11
constexpr uint32_t VTF_SIGNATURE
Definition: VTF.h:21
constexpr uint32_t VTFX_SIGNATURE
Definition: VTF.h:22
constexpr uint32_t VTF3_SIGNATURE
Definition: VTF.h:23
CompressionMethod
Definition: VTF.h:25
ImageFormat
Definition: ImageFormats.h:7
static consteval std::array< Type, 9 > getOrder()
Definition: VTF.h:47
Type type
Definition: VTF.h:66
ConvertedData convertData() const
Definition: VTF.cpp:177
std::variant< std::monostate, SHT, uint32_t, std::tuple< uint8_t, uint8_t, uint8_t, uint8_t >, std::string, HOT, std::span< uint32_t > > ConvertedData
Definition: VTF.h:78
@ FLAG_LOCAL_DATA
Definition: VTF.h:63
@ FLAG_NONE
Definition: VTF.h:62
@ TYPE_KEYVALUES_DATA
Definition: VTF.h:45
@ TYPE_CRC
Definition: VTF.h:42
@ TYPE_PARTICLE_SHEET_DATA
Definition: VTF.h:38
@ TYPE_EXTENDED_FLAGS
Definition: VTF.h:41
@ TYPE_IMAGE_DATA
Definition: VTF.h:40
@ TYPE_AUX_COMPRESSION
Definition: VTF.h:43
@ TYPE_LOD_CONTROL_INFO
Definition: VTF.h:44
@ TYPE_THUMBNAIL_DATA
Definition: VTF.h:37
@ TYPE_HOTSPOT_DATA
Definition: VTF.h:39
Flags flags
Definition: VTF.h:67
std::span< std::byte > data
Definition: VTF.h:68
uint16_t initialFrameCount
Definition: VTF.h:231
uint16_t initialSliceCount
Definition: VTF.h:234
int16_t compressionLevel
Definition: VTF.h:240
ImageConversion::ResizeFilter filter
Definition: VTF.h:229
ImageConversion::ResizeMethod heightResizeMethod
Definition: VTF.h:228
CompressionMethod compressionMethod
Definition: VTF.h:241
ImageConversion::ResizeMethod widthResizeMethod
Definition: VTF.h:227
ImageFormat outputFormat
Definition: VTF.h:226