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>  |
28 | namespace tImage  |
29 | {  |
30 |   |
31 |   |
32 | bool tTexture::BC7EncInitialized = false;  |
33 |   |
34 |   |
35 | bool 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 |   |
49 | bool 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 |   |
63 | bool 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 |   |
127 | bool 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 |   |
138 | bool 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 |   |
203 | void 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 |   |
253 | void 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 |   |
304 | void 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 |   |
391 | int 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 |   |
408 | void 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 |   |
429 | void 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 |   |
457 | bool 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 | |