| 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 | |