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