1// tTexture.cpp 
2// 
3// A tTexture is a 'hardware-ready' format. tTextures contain functionality for creating mipmap layers in a variety of 
4// block-compressed and uncompressed formats. A tTexture stores each mipmap layer in a tLayer. A tTexture can be 
5// created from either a tPicture or a dds file. The purpose of a dds file is so that content-creators have control 
6// over the authoring of each mipmap level and the exact pixel format used. Basically if you've created a dds file, 
7// you're saying you want the final hardware to use the image data unchanged and as authored -- same mip levels, same 
8// pixel format, same dimensions. For this reason, dds files should not be loaded into tPictures where image 
9// manipulation occurs and possibly lossy block-compressed dds images would be decompressed. A dds file may contain more 
10// than one image if it is a cubemap, but a tTexture only ever represents a single image. The tTexture dds constructor 
11// allows you to decide which one gets loaded. tTextures can save and load to a tChunk-based format, and are therefore 
12// useful at both pipeline and for runtime loading. To save to a tChunk file format a tTexture will call the Save 
13// method of all the tLayers. 
14// 
15// Copyright (c) 2006, 2016, 2017, 2020 Tristan Grimmer. 
16// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
17// granted, provided that the above copyright notice and this permission notice appear in all copies. 
18// 
19// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
21// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
22// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
23// PERFORMANCE OF THIS SOFTWARE. 
24 
25#include <Image/tTexture.h> 
26#define RGBCX_IMPLEMENTATION 
27#include <BC7Enc/rgbcx.h> 
28namespace tImage 
29{ 
30 
31 
32bool tTexture::BC7EncInitialized = false; 
33 
34 
35bool tTexture::Set(tList<tLayer>& layers) 
36{ 
37 Clear(); 
38 if (layers.GetNumItems() == 0) 
39 return false; 
40 
41 while (!layers.IsEmpty()) 
42 Layers.Append(layers.Remove()); 
43 
44 Opaque = Layers.First()->IsOpaqueFormat(); 
45 return true; 
46} 
47 
48 
49bool tTexture::Load(const tString& ddsFile, tImageDDS::tSurfIndex surface, bool correctRowOrder) 
50{ 
51 Clear(); 
52 if ((tSystem::tGetFileType(ddsFile) != tSystem::tFileType::DDS) || !tSystem::tFileExists(ddsFile)) 
53 return false; 
54 
55 tImageDDS dds(ddsFile, correctRowOrder); 
56 if (!dds.IsValid()) 
57 return false; 
58 
59 return Set(dds, surface); 
60} 
61 
62 
63bool tTexture::Set(tImageDDS& dds, tImageDDS::tSurfIndex surface) 
64{ 
65 Clear(); 
66 if (!dds.IsValid()) 
67 return false; 
68 
69 if (!dds.IsCubemap()) 
70 { 
71 dds.StealTextureLayers(Layers); 
72 } 
73 else 
74 { 
75 tList<tLayer> layerSets[tImageDDS::tSurfIndex_NumSurfaces]; 
76 dds.StealCubemapLayers(layerSets); 
77 while (!layerSets[surface].IsEmpty()) 
78 Layers.Append(layerSets[surface].Remove()); 
79 } 
80 
81 if (Layers.GetNumItems() == 0) 
82 return false; 
83 
84 Opaque = Layers.First()->IsOpaqueFormat(); 
85 
86 // Does the dds texture have power-of-2 dimensions? For dds textures we never do a decompress and recompress (loss 
87 // of quality, time consuming, and misses the point of using dds files in the first place). That is, we treat 
88 // direct surfaces as "direct" textures. For this reason we require them to have power-of-2 dimensions and do not 
89 // resample them. It is a catastrophic error if the DDS does not have power-of-2 dimensions. 
90 tLayer* mainLayer = Layers.First(); 
91 int width = mainLayer->Width; 
92 int height = mainLayer->Height; 
93 
94 if (!tMath::tIsPower2(width) || !tMath::tIsPower2(height)) 
95 { 
96 int nearestWidth = tMath::tClosestPower2(width); 
97 int nearestHeight = tMath::tClosestPower2(height); 
98 
99 Clear(); 
100 throw tError 
101 ( 
102 "Direct Draw texture '%s' must have power-of-2 dimensions. " 
103 "Direct Draw textures do not get automatically resized by the pipeline. " 
104 "The current dimensions are %dx%d. The suggested size to make the texture is %dx%d.", 
105 tSystem::tGetFileBaseName(dds.Filename).Pod(), width, height, nearestWidth, nearestHeight 
106 ); 
107 } 
108 
109 if 
110 ( 
111 !tMath::tInRange(width, tLayer::MinLayerDimension, tLayer::MaxLayerDimension) || 
112 !tMath::tInRange(height, tLayer::MinLayerDimension, tLayer::MaxLayerDimension) 
113 ) 
114 { 
115 Clear(); 
116 throw tError 
117 ( 
118 "Direct Draw texture '%s' has dimensions %dx%d but must have texture dimensions within the allowable range of [%d, %d].", 
119 tSystem::tGetFileBaseName(dds.Filename).Pod(), width, height, tLayer::MinLayerDimension, tLayer::MaxLayerDimension 
120 ); 
121 } 
122 
123 return true; 
124} 
125 
126 
127bool tTexture::Load(const tString& imageFile, bool generateMipMaps, tPixelFormat format, tQuality quality, int forceWidth, int forceHeight) 
128{ 
129 Clear(); 
130 tPicture image(imageFile); 
131 if (!image.IsValid()) 
132 return false; 
133 
134 return Set(image, generateMipMaps, format, quality, forceWidth, forceHeight); 
135} 
136 
137 
138bool tTexture::Set(tPicture& image, bool generateMipmaps, tPixelFormat pixelFormat, tQuality quality, int forceWidth, int forceHeight) 
139{ 
140 Clear(); 
141 
142 // Sanity check force arguments. 
143 if (forceWidth && !tMath::tIsPower2(forceWidth)) 
144 throw tError("Texture forceWidth was specified but is not a power of 2."); 
145 
146 if (forceHeight && !tMath::tIsPower2(forceHeight)) 
147 throw tError("Texture forceHeight was specified but is not a power of 2."); 
148 
149 // If the dimensions are incorrect we choose the closest power of 2 to resample to. Eg. If the value is 54 we can 
150 // choose from 32 and 64, but since 32 is 22 away and 64 is only 10, we choose 64. 
151 int origWidth = image.GetWidth(); 
152 int newWidth = forceWidth ? forceWidth : tMath::tClosestPower2(origWidth); 
153 tMath::tiClamp(newWidth, tLayer::MinLayerDimension, tLayer::MaxLayerDimension); 
154 
155 int origHeight = image.GetHeight(); 
156 int newHeight = forceHeight ? forceHeight : tMath::tClosestPower2(origHeight); 
157 tMath::tiClamp(newHeight, tLayer::MinLayerDimension, tLayer::MaxLayerDimension); 
158 
159 if ((origWidth != newWidth) || (origHeight != newHeight)) 
160 { 
161 // Might want to let user know that we're resampling here. This resize happens when the artist didn't submit 
162 // proper power-of-2-sized images or if dimensions were forced. 
163 bool ok = image.Resize(newWidth, newHeight, DetermineFilter(quality)); 
164 if (!ok) 
165 throw tError("Problem resampling texture '%s' to %dx%d.", tSystem::tGetFileBaseName(image.Filename).Pod(), newWidth, newHeight); 
166 } 
167 
168 // This must be set before AutoDeterminePixelFormat is called. 
169 Opaque = image.IsOpaque(); 
170 
171 // Are we supposed to automatically determine the pixel format? 
172 if (pixelFormat == tPixelFormat::Auto) 
173 pixelFormat = DeterminePixelFormat(image); 
174 
175 switch (pixelFormat) 
176 { 
177 case tPixelFormat::R8G8B8: 
178 case tPixelFormat::R8G8B8A8: 
179 ProcessImageTo_R8G8B8_Or_R8G8B8A8(image, pixelFormat, generateMipmaps, quality); 
180 break; 
181 
182 case tPixelFormat::G3B5R5G3: 
183 ProcessImageTo_G3B5R5G3(image, generateMipmaps, quality); 
184 break; 
185 
186 case tPixelFormat::BC1_DXT1BA: 
187 case tPixelFormat::BC1_DXT1: 
188 case tPixelFormat::BC2_DXT3: 
189 case tPixelFormat::BC3_DXT5: 
190 ProcessImageTo_BCTC(image, pixelFormat, generateMipmaps, quality); 
191 break; 
192 
193 default: 
194 throw tError("Conversion of image to pixel format %d failed.", int(pixelFormat)); 
195 } 
196 
197 // Since the convert functions may or may not modify the source tPicture image, we guarantee invalidness here. 
198 image.Clear(); 
199 return true; 
200} 
201 
202 
203void tTexture::ProcessImageTo_R8G8B8_Or_R8G8B8A8(tPicture& image, tPixelFormat format, bool generateMipmaps, tQuality quality) 
204{ 
205 tAssert((format == tPixelFormat::R8G8B8) || (format == tPixelFormat::R8G8B8A8)); 
206 int width = image.GetWidth(); 
207 int height = image.GetHeight(); 
208 int bytesPerPixel = (format == tPixelFormat::R8G8B8) ? 3 : 4; 
209 tResampleFilter filter = DetermineFilter(quality); 
210 
211 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
212 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
213 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
214 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
215 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
216 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
217 while (1) 
218 { 
219 int numDataBytes = width*height*bytesPerPixel; 
220 uint8* layerData = new uint8[numDataBytes]; 
221 
222 // We can just extract the data out directly from RGBA to either RGB or RGBA. 
223 uint8* srcPixel = (uint8*)image.GetPixelPointer(); 
224 uint8* dstPixel = layerData; 
225 for (int p = 0; p < width*height; p++) 
226 { 
227 tStd::tMemcpy(dstPixel, srcPixel, bytesPerPixel); 
228 srcPixel += 4; // Src is always RGBA. 
229 dstPixel += bytesPerPixel; // Dst is RGB or RGBA. 
230 } 
231 
232 tLayer* layer = new tLayer(format, width, height, layerData, true); 
233 tAssert(numDataBytes == layer->GetDataSize()); 
234 Layers.Append(layer); 
235 
236 // Was this the last one? 
237 if (((width == 1) && (height == 1)) || !generateMipmaps) 
238 break; 
239 
240 // Remember, width and height are not necessarily the same. As soon as one reaches 1 it needs to stay there until 
241 // the other gets there too. 
242 if (width != 1) 
243 width >>= 1; 
244 
245 if (height != 1) 
246 height >>= 1; 
247 
248 image.Resize(width, height, filter); 
249 } 
250} 
251 
252 
253void tTexture::ProcessImageTo_G3B5R5G3(tPicture& image, bool generateMipmaps, tQuality quality) 
254{ 
255 int width = image.GetWidth(); 
256 int height = image.GetHeight(); 
257 int bytesPerPixel = 2; 
258 tResampleFilter filter = DetermineFilter(quality); 
259 
260 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
261 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
262 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
263 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
264 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
265 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
266 while (1) 
267 { 
268 int numDataBytes = width*height*bytesPerPixel; 
269 uint8* layerData = new uint8[numDataBytes]; 
270 
271 // We need to change the src data (RGBA) into 16bits. 
272 tPixel* srcPixel = image.GetPixelPointer(); 
273 uint8* dstPixel = layerData; 
274 for (int p = 0; p < width*height; p++) 
275 { 
276 // In memory. Each letter a bit: GGGBBBBB RRRRRGGG 
277 dstPixel[0] = (srcPixel->G & 0x1C << 3) | (srcPixel->B >> 3); 
278 dstPixel[1] = (srcPixel->R & 0xF8) | (srcPixel->G >> 5); 
279 srcPixel++; 
280 dstPixel += bytesPerPixel; 
281 } 
282 
283 tLayer* layer = new tLayer(tPixelFormat::G3B5R5G3, width, height, layerData, true); 
284 tAssert(numDataBytes == layer->GetDataSize()); 
285 Layers.Append(layer); 
286 
287 // Was this the last one? 
288 if (((width == 1) && (height == 1)) || !generateMipmaps) 
289 break; 
290 
291 // Remember, width and height are not necessarily the same. As soon as one reaches 1 it needs to stay there until 
292 // the other gets there too. 
293 if (width != 1) 
294 width >>= 1; 
295 
296 if (height != 1) 
297 height >>= 1; 
298 
299 image.Resize(width, height, filter); 
300 } 
301} 
302 
303 
304void tTexture::ProcessImageTo_BCTC(tPicture& image, tPixelFormat pixelFormat, bool generateMipmaps, tQuality quality) 
305{ 
306 int width = image.GetWidth(); 
307 int height = image.GetHeight(); 
308 tResampleFilter filter = DetermineFilter(quality); 
309 if (!tMath::tIsPower2(width) || !tMath::tIsPower2(height)) 
310 throw tError("Texture must be power-of-2 to be compressed to a BC format."); 
311 
312 if (!BC7EncInitialized) 
313 { 
314 rgbcx::init(rgbcx::bc1_approx_mode::cBC1Ideal); 
315 BC7EncInitialized = true; 
316 } 
317 
318 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
319 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
320 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
321 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
322 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
323 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
324 while (1) 
325 { 
326 // Setup the layer data to receive the compressed data. 
327 int numBlocks = tMath::tMax(1, width/4) * tMath::tMax(1, height/4); 
328 int blockSize = (pixelFormat == tPixelFormat::BC1_DXT1) ? 8 : 16; 
329 int outputSize = numBlocks * blockSize; 
330 uint8* outputData = new uint8[outputSize]; 
331 
332 int encoderQualityLevel = DetermineBlockEncodeQualityLevel(quality); 
333 bool allow3colour = true; 
334 bool useTransparentTexelsForBlack = false; 
335 
336 uint8* blockDest = outputData; 
337 uint8* pixelSrc = (uint8*)image.GetPixelPointer(); 
338 for (int block = 0; block < numBlocks; block++) 
339 { 
340 switch (pixelFormat) 
341 { 
342 case tPixelFormat::BC1_DXT1: 
343 rgbcx::encode_bc1(encoderQualityLevel, blockDest, pixelSrc, allow3colour, useTransparentTexelsForBlack); 
344 break; 
345 
346 case tPixelFormat::BC3_DXT5: 
347 rgbcx::encode_bc3(encoderQualityLevel, blockDest, pixelSrc); 
348 break; 
349 
350 default: 
351 throw tError("Unsupported BC pixel format %d.", int(pixelFormat)); 
352 } 
353 blockDest += blockSize; 
354 pixelSrc += sizeof(tPixel); 
355 } 
356 
357 // The last true in this call allows the layer constructor to steal the outputData pointer. Avoids extra memcpys. 
358 tLayer* layer = new tLayer(pixelFormat, width, height, outputData, true); 
359 tAssert(layer->GetDataSize() == outputSize); 
360 Layers.Append(layer); 
361 
362 // Was this the last one? 
363 if (((width == 1) && (height == 1)) || !generateMipmaps) 
364 break; 
365 
366 if (width != 1) 
367 width >>= 1; 
368 
369 if (height != 1) 
370 height >>= 1; 
371 
372 // When using BC compression we don't ever want to scale lower than 4x4 as that is the individual block size. 
373 // we need at least that much data so the compressor can do it's job. Consider a 128x4 texture: Ideally we want 
374 // that to rescale to 64x4, rather than 64x2. So it's reasonable to just stop once either dimension reaches 4 
375 // because otherwise non-uniform scale issues come into play. In short, we either have to deal with this 
376 // distortion, or the cropping issue of just stopping. We do the latter because it's just easier. 
377 // 
378 // Just because we stop downscaling doesn't mean that we don't generate all the mipmap levels! We still 
379 // generate all the way to 1x1. It's only the src data that stops being down-sampled. 
380 if ((image.GetWidth() >= 8) && (image.GetHeight() >= 8)) 
381 { 
382 // This code scales by half using the correct quality filter. 
383 int newWidth = image.GetWidth() / 2; 
384 int newHeight = image.GetHeight() / 2; 
385 image.Resize(newWidth, newHeight, filter); 
386 } 
387 } 
388} 
389 
390 
391int tTexture::ComputeMaxNumberOfMipmaps() const 
392{ 
393 if (!IsValid()) 
394 return 0; 
395 
396 int maxDim = tMath::tMax(GetWidth(), GetHeight()); 
397 int count = 0; 
398 while (maxDim > 0) 
399 { 
400 maxDim >>= 1; 
401 count++; 
402 } 
403 
404 return count; 
405} 
406 
407 
408void tTexture::Save(tChunkWriter& chunk) const 
409{ 
410 chunk.Begin(tChunkID::Image_Texture); 
411 { 
412 chunk.Begin(tChunkID::Image_TextureProperties); 
413 { 
414 chunk.Write(Opaque); 
415 } 
416 chunk.End(); 
417 
418 chunk.Begin(tChunkID::Image_TextureLayers); 
419 { 
420 for (tLayer* layer = Layers.First(); layer; layer = layer->Next()) 
421 layer->Save(chunk); 
422 } 
423 chunk.End(); 
424 } 
425 chunk.End(); 
426} 
427 
428 
429void tTexture::Load(const tChunk& chunk) 
430{ 
431 Clear(); 
432 if (chunk.ID() != tChunkID::Image_Texture) 
433 return; 
434 
435 int numLayers = 0; 
436 for (tChunk ch = chunk.First(); ch.IsValid(); ch = ch.Next()) 
437 { 
438 switch (ch.ID()) 
439 { 
440 case tChunkID::Image_TextureProperties: 
441 { 
442 ch.GetItem(Opaque); 
443 break; 
444 } 
445 
446 case tChunkID::Image_TextureLayers: 
447 { 
448 for (tChunk layerChunk = ch.First(); layerChunk.IsValid(); layerChunk = layerChunk.Next()) 
449 Layers.Append(new tLayer(layerChunk)); 
450 break; 
451 } 
452 } 
453 } 
454} 
455 
456 
457bool tTexture::operator==(const tTexture& src) const 
458{ 
459 if (!IsValid() || !src.IsValid()) 
460 return false; 
461 
462 if (Opaque != src.Opaque) 
463 return false; 
464 
465 if (Layers.GetNumItems() != Layers.GetNumItems()) 
466 return false; 
467 
468 tLayer* srcLayer = Layers.First(); 
469 for (tLayer* layer = Layers.First(); layer; layer = layer->Next(), srcLayer = srcLayer->Next()) 
470 if (*layer != *srcLayer) 
471 return false; 
472 
473 return true; 
474} 
475 
476 
477} 
478