// texture.cpp // // Copyright (C) 2001-2003, Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. #include #include #include #include #include #include #include #include "glsupport.h" #include #include #include #include "framebuffer.h" #include "texture.h" #include "virtualtex.h" using namespace celestia; using namespace Eigen; using namespace std; using celestia::util::GetLogger; struct TextureCaps { GLint preferredAnisotropy; }; static TextureCaps texCaps; #undef DUMP_TEXTURE_MIPMAP_INFO #ifdef DUMP_TEXTURE_MIPMAP_INFO static void DumpTextureMipmapInfo(GLenum target) { for (int i = 0; i < 16; i++) { GLint w = 0, h = 0; glGetTexLevelParameteriv(target, i, GL_TEXTURE_HEIGHT, &h); if (glGetError() != GL_NO_ERROR) break; glGetTexLevelParameteriv(target, i, GL_TEXTURE_WIDTH, &w); if (glGetError() != GL_NO_ERROR) break; if (w == 0 || h == 0) break; cout << w << 'x' << h << '\n'; } } #else #define DumpTextureMipmapInfo(target) (void)target #endif static bool testMaxLevel() { #ifndef GL_ES unsigned char texels[64]; GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); // Test whether GL_TEXTURE_MAX_LEVEL is supported . . . glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 8, 8, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, texels); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2); float maxLev = -1.0f; glGetTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, &maxLev); glDeleteTextures(1, &textureID); return maxLev == 2; #else return false; #endif } static const TextureCaps& GetTextureCaps() { static bool texCapsInitialized = false; if (!texCapsInitialized) { texCapsInitialized = true; texCaps.preferredAnisotropy = 1; #ifndef GL_ES if (gl::EXT_texture_filter_anisotropic) { // Cap the preferred level texture anisotropy to 8; eventually, we should allow // the user to control this. texCaps.preferredAnisotropy = min(8, gl::maxTextureAnisotropy); } #endif } return texCaps; } static int getInternalFormat(PixelFormat format) { #ifdef GL_ES switch (format) { case PixelFormat::RGBA: case PixelFormat::RGB: case PixelFormat::LUM_ALPHA: case PixelFormat::ALPHA: case PixelFormat::LUMINANCE: case PixelFormat::DXT1: case PixelFormat::DXT3: case PixelFormat::DXT5: return (int) format; default: return 0; } #else switch (format) { case PixelFormat::RGBA: case PixelFormat::BGRA: case PixelFormat::RGB: case PixelFormat::BGR: case PixelFormat::LUM_ALPHA: case PixelFormat::ALPHA: case PixelFormat::LUMINANCE: case PixelFormat::DXT1: case PixelFormat::DXT3: case PixelFormat::DXT5: return (int) format; default: return 0; } #endif } #if 0 // Required in order to support on-the-fly compression; currently, this // feature is disabled. static int getCompressedInternalFormat(int format) { switch (format) { case GL_RGB: case GL_BGR: return GL_COMPRESSED_RGB; case GL_RGBA: case GL_BGRA: return GL_COMPRESSED_RGBA; case GL_ALPHA: return GL_COMPRESSED_ALPHA; case GL_LUMINANCE: return GL_COMPRESSED_LUMINANCE; case GL_LUMINANCE_ALPHA: return GL_COMPRESSED_LUMINANCE_ALPHA; case GL_INTENSITY: return GL_COMPRESSED_INTENSITY; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: return format; default: return 0; } } #endif static int getCompressedBlockSize(PixelFormat format) { return format == PixelFormat::DXT1 ? 8 : 16; } static GLenum GetGLTexAddressMode(Texture::AddressMode addressMode) { switch (addressMode) { case Texture::Wrap: return GL_REPEAT; case Texture::EdgeClamp: return GL_CLAMP_TO_EDGE; case Texture::BorderClamp: #ifndef GL_ES return GL_CLAMP_TO_BORDER; #else return GL_CLAMP_TO_BORDER_OES; #endif } return 0; } static void SetBorderColor(Color borderColor, GLenum target) { float bc[4] = { borderColor.red(), borderColor.green(), borderColor.blue(), borderColor.alpha() }; #ifndef GL_ES glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, bc); #else glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR_OES, bc); #endif } // Load a prebuilt set of mipmaps; assumes that the image contains // a complete set of mipmap levels. static void LoadMipmapSet(Image& img, GLenum target) { int internalFormat = getInternalFormat(img.getFormat()); for (int mip = 0; mip < img.getMipLevelCount(); mip++) { unsigned int mipWidth = max((unsigned int) img.getWidth() >> mip, 1u); unsigned int mipHeight = max((unsigned int) img.getHeight() >> mip, 1u); if (img.isCompressed()) { glCompressedTexImage2D(target, mip, internalFormat, mipWidth, mipHeight, 0, img.getMipLevelSize(mip), img.getMipLevel(mip)); } else { glTexImage2D(target, mip, internalFormat, mipWidth, mipHeight, 0, (GLenum) img.getFormat(), GL_UNSIGNED_BYTE, img.getMipLevel(mip)); } } } // Load a texture without any mipmaps static void LoadMiplessTexture(Image& img, GLenum target) { int internalFormat = getInternalFormat(img.getFormat()); if (img.isCompressed()) { glCompressedTexImage2D(target, 0, internalFormat, img.getWidth(), img.getHeight(), 0, img.getMipLevelSize(0), img.getMipLevel(0)); } else { glTexImage2D(target, 0, internalFormat, img.getWidth(), img.getHeight(), 0, (GLenum) img.getFormat(), GL_UNSIGNED_BYTE, img.getMipLevel(0)); } } static int ilog2(unsigned int x) { int n = -1; while (x != 0) { x >>= 1; n++; } return n; } static int CalcMipLevelCount(int w, int h) { return max(ilog2(w), ilog2(h)) + 1; } Texture::Texture(int w, int h, int d) : width(w), height(h), depth(d) { } int Texture::getLODCount() const { return 1; } int Texture::getUTileCount(int /*unused*/) const { return 1; } int Texture::getVTileCount(int /*unused*/) const { return 1; } int Texture::getWTileCount(int /*unused*/) const { return 1; } void Texture::setBorderColor(Color /*unused*/) { } int Texture::getWidth() const { return width; } int Texture::getHeight() const { return height; } int Texture::getDepth() const { return depth; } unsigned int Texture::getFormatOptions() const { return formatOptions; } void Texture::setFormatOptions(unsigned int opts) { formatOptions = opts; } ImageTexture::ImageTexture(Image& img, AddressMode addressMode, MipMapMode mipMapMode) : Texture(img.getWidth(), img.getHeight()), glName(0) { glGenTextures(1, (GLuint*) &glName); glBindTexture(GL_TEXTURE_2D, glName); bool mipmap = mipMapMode != NoMipMaps; bool precomputedMipMaps = false; // Use precomputed mipmaps only if a complete set is supplied int mipLevelCount = img.getMipLevelCount(); if (mipmap && mipLevelCount == CalcMipLevelCount(img.getWidth(), img.getHeight())) { precomputedMipMaps = true; } // We can't automatically generate mipmaps for compressed textures. // If a precomputed mipmap set isn't provided, turn off mipmapping entirely. if (!precomputedMipMaps && img.isCompressed()) { mipmap = false; } GLenum texAddress = GetGLTexAddressMode(addressMode); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texAddress); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texAddress); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR); #ifndef GL_ES if (gl::EXT_texture_filter_anisotropic && texCaps.preferredAnisotropy > 1) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, texCaps.preferredAnisotropy); } #endif bool genMipmaps = mipmap && !precomputedMipMaps; #if !defined(GL_ES) if (genMipmaps && !FramebufferObject::isSupported()) glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); #endif if (mipmap) { if (precomputedMipMaps) LoadMipmapSet(img, GL_TEXTURE_2D); else if (mipMapMode == DefaultMipMaps) LoadMiplessTexture(img, GL_TEXTURE_2D); } else { LoadMiplessTexture(img, GL_TEXTURE_2D); } if (genMipmaps && FramebufferObject::isSupported()) glGenerateMipmap(GL_TEXTURE_2D); DumpTextureMipmapInfo(GL_TEXTURE_2D); alpha = img.hasAlpha(); compressed = img.isCompressed(); } ImageTexture::~ImageTexture() { if (glName != 0) glDeleteTextures(1, (const GLuint*) &glName); } void ImageTexture::bind() { glBindTexture(GL_TEXTURE_2D, glName); } const TextureTile ImageTexture::getTile(int lod, int u, int v) { if (lod != 0 || u != 0 || v != 0) return TextureTile(0); return TextureTile(glName); } unsigned int ImageTexture::getName() const { return glName; } void ImageTexture::setBorderColor(Color borderColor) { bind(); SetBorderColor(borderColor, GL_TEXTURE_2D); } TiledTexture::TiledTexture(Image& img, int _uSplit, int _vSplit, MipMapMode mipMapMode) : Texture(img.getWidth(), img.getHeight()), uSplit(_uSplit), vSplit(_vSplit), glNames(nullptr) { glNames = new unsigned int[uSplit * vSplit]; { for (int i = 0; i < uSplit * vSplit; i++) glNames[i] = 0; } alpha = img.hasAlpha(); compressed = img.isCompressed(); bool mipmap = mipMapMode != NoMipMaps; bool precomputedMipMaps = false; // Require a complete set of mipmaps int mipLevelCount = img.getMipLevelCount(); int completeMipCount = CalcMipLevelCount(img.getWidth(), img.getHeight()); // Allow a bit of slack here--it turns out that some tools don't want to // calculate the 1x1 mip level. Rather than turn off mipmaps, we'll just // point the 1x1 mip to the 2x1. if (mipmap && mipLevelCount >= completeMipCount - 1) precomputedMipMaps = true; // We can't automatically generate mipmaps for compressed textures. // If a precomputed mipmap set isn't provided, turn of mipmapping entirely. if (!precomputedMipMaps && img.isCompressed()) mipmap = false; GLenum texAddress = GetGLTexAddressMode(EdgeClamp); int components = img.getComponents(); // Create a temporary image which we'll use for the tile texels int tileWidth = img.getWidth() / uSplit; int tileHeight = img.getHeight() / vSplit; int tileMipLevelCount = CalcMipLevelCount(tileWidth, tileHeight); Image* tile = new Image(img.getFormat(), tileWidth, tileHeight, tileMipLevelCount); for (int v = 0; v < vSplit; v++) { for (int u = 0; u < uSplit; u++) { // Create the texture and set up sampling and addressing glGenTextures(1, (GLuint*)&glNames[v * uSplit + u]); glBindTexture(GL_TEXTURE_2D, glNames[v * uSplit + u]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, texAddress); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, texAddress); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR); #ifndef GL_ES if (gl::EXT_texture_filter_anisotropic && texCaps.preferredAnisotropy > 1) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, texCaps.preferredAnisotropy); } #endif // Copy texels from the subtexture area to the pixel buffer. This // is straightforward for normal textures, but an immense headache // for compressed textures with prebuilt mipmaps. if (precomputedMipMaps) { if (img.isCompressed()) { for (int mip = 0; mip < tileMipLevelCount; mip++) { int blockSize = getCompressedBlockSize(img.getFormat()); unsigned char* imgMip = img.getMipLevel(min(mip, mipLevelCount)); unsigned int mipWidth = max((unsigned int) img.getWidth() >> mip, 1u); unsigned char* tileMip = tile->getMipLevel(mip); unsigned int tileMipWidth = max((unsigned int) tile->getWidth() >> mip, 1u); unsigned int tileMipHeight = max((unsigned int) tile->getHeight() >> mip, 1u); int uBlocks = max(tileMipWidth / 4, 1u); int vBlocks = max(tileMipHeight / 4, 1u); int destBytesPerRow = uBlocks * blockSize; int srcBytesPerRow = max(mipWidth / 4, 1u) * blockSize; int srcU = u * tileMipWidth / 4; int srcV = v * tileMipHeight / 4; int tileOffset = srcV * srcBytesPerRow + srcU * blockSize; for (int y = 0; y < vBlocks; y++) { memcpy(tileMip + y * destBytesPerRow, imgMip + tileOffset + y * srcBytesPerRow, destBytesPerRow); } } } else { // TODO: Handle uncompressed textures with prebuilt mipmaps } LoadMipmapSet(*tile, GL_TEXTURE_2D); } else { if (img.isCompressed()) { int blockSize = getCompressedBlockSize(img.getFormat()); int uBlocks = max(tileWidth / 4, 1); int vBlocks = max(tileHeight / 4, 1); int destBytesPerRow = uBlocks * blockSize; int srcBytesPerRow = max(img.getWidth() / 4, 1) * blockSize; int srcU = u * tileWidth / 4; int srcV = v * tileHeight / 4; int tileOffset = srcV * srcBytesPerRow + srcU * blockSize; for (int y = 0; y < vBlocks; y++) { memcpy(tile->getPixels() + y * destBytesPerRow, img.getPixels() + tileOffset + y * srcBytesPerRow, destBytesPerRow); } } else { unsigned char* tilePixels = img.getPixels() + (v * tileHeight * img.getWidth() + u * tileWidth) * components; for (int y = 0; y < tileHeight; y++) { memcpy(tile->getPixels() + y * tileWidth * components, tilePixels + y * img.getWidth() * components, tileWidth * components); } } if (mipmap) { if (FramebufferObject::isSupported()) { LoadMiplessTexture(*tile, GL_TEXTURE_2D); glGenerateMipmap(GL_TEXTURE_2D); } #ifndef GL_ES else { glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); LoadMiplessTexture(*tile, GL_TEXTURE_2D); } #endif } else { LoadMiplessTexture(*tile, GL_TEXTURE_2D); } DumpTextureMipmapInfo(GL_TEXTURE_2D); } } } delete tile; } TiledTexture::~TiledTexture() { if (glNames != nullptr) { for (int i = 0; i < uSplit * vSplit; i++) { if (glNames[i] != 0) glDeleteTextures(1, (const GLuint*) &glNames[i]); } delete[] glNames; } } void TiledTexture::bind() { } void TiledTexture::setBorderColor(Color borderColor) { for (int i = 0; i < vSplit; i++) { for (int j = 0; j < uSplit; j++) { glBindTexture(GL_TEXTURE_2D, glNames[i * uSplit + j]); SetBorderColor(borderColor, GL_TEXTURE_2D); } } } int TiledTexture::getUTileCount(int /*lod*/) const { return uSplit; } int TiledTexture::getVTileCount(int /*lod*/) const { return vSplit; } const TextureTile TiledTexture::getTile(int lod, int u, int v) { if (lod != 0 || u >= uSplit || u < 0 || v >= vSplit || v < 0) return TextureTile(0); return TextureTile(glNames[v * uSplit + u]); } CubeMap::CubeMap(Image* faces[]) : Texture(faces[0]->getWidth(), faces[0]->getHeight()), glName(0) { // Verify that all the faces are square and have the same size int width = faces[0]->getWidth(); PixelFormat format = faces[0]->getFormat(); int i = 0; for (i = 0; i < 6; i++) { if (faces[i]->getWidth() != width || faces[i]->getHeight() != width || faces[i]->getFormat() != format) return; } // For now, always enable mipmaps; in the future, it should be possible to // override this. bool mipmap = true; bool precomputedMipMaps = false; // Require a complete set of mipmaps int mipLevelCount = faces[0]->getMipLevelCount(); if (mipmap && mipLevelCount == CalcMipLevelCount(width, width)) precomputedMipMaps = true; // We can't automatically generate mipmaps for compressed textures. // If a precomputed mipmap set isn't provided, turn of mipmapping entirely. if (!precomputedMipMaps && faces[0]->isCompressed()) mipmap = false; glGenTextures(1, (GLuint*) &glName); glBindTexture(GL_TEXTURE_CUBE_MAP, glName); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR); bool genMipmaps = mipmap && !precomputedMipMaps; #if !defined(GL_ES) if (genMipmaps && !FramebufferObject::isSupported()) glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_GENERATE_MIPMAP, GL_TRUE); #endif for (i = 0; i < 6; i++) { auto targetFace = (GLenum) ((int) GL_TEXTURE_CUBE_MAP_POSITIVE_X + i); Image* face = faces[i]; if (mipmap) { if (precomputedMipMaps) LoadMipmapSet(*face, targetFace); else LoadMiplessTexture(*face, targetFace); } else { LoadMiplessTexture(*face, targetFace); } } if (genMipmaps && FramebufferObject::isSupported()) glGenerateMipmap(GL_TEXTURE_CUBE_MAP); DumpTextureMipmapInfo(GL_TEXTURE_CUBE_MAP_POSITIVE_X); } CubeMap::~CubeMap() { if (glName != 0) glDeleteTextures(1, (const GLuint*) &glName); } void CubeMap::bind() { glBindTexture(GL_TEXTURE_CUBE_MAP, glName); } const TextureTile CubeMap::getTile(int lod, int u, int v) { if (lod != 0 || u != 0 || v != 0) return TextureTile(0); return TextureTile(glName); } void CubeMap::setBorderColor(Color borderColor) { bind(); SetBorderColor(borderColor, GL_TEXTURE_CUBE_MAP); } Texture* CreateProceduralTexture(int width, int height, PixelFormat format, ProceduralTexEval func, Texture::AddressMode addressMode, Texture::MipMapMode mipMode) { Image* img = new Image(format, width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float u = ((float) x + 0.5f) / (float) width * 2 - 1; float v = ((float) y + 0.5f) / (float) height * 2 - 1; func(u, v, 0, img->getPixelRow(y) + x * img->getComponents()); } } Texture* tex = new ImageTexture(*img, addressMode, mipMode); delete img; return tex; } Texture* CreateProceduralTexture(int width, int height, PixelFormat format, TexelFunctionObject& func, Texture::AddressMode addressMode, Texture::MipMapMode mipMode) { Image* img = new Image(format, width, height); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float u = ((float) x + 0.5f) / (float) width * 2 - 1; float v = ((float) y + 0.5f) / (float) height * 2 - 1; func(u, v, 0, img->getPixelRow(y) + x * img->getComponents()); } } Texture* tex = new ImageTexture(*img, addressMode, mipMode); delete img; return tex; } // Helper function for CreateProceduralCubeMap; return the normalized // vector pointing to (s, t) on the specified face. static Vector3f cubeVector(int face, float s, float t) { Vector3f v; switch (face) { case 0: v = Vector3f(1.0f, -t, -s); break; case 1: v = Vector3f(-1.0f, -t, s); break; case 2: v = Vector3f(s, 1.0f, t); break; case 3: v = Vector3f(s, -1.0f, -t); break; case 4: v = Vector3f(s, -t, 1.0f); break; case 5: v = Vector3f(-s, -t, -1.0f); break; default: // assert(false); break; } v.normalize(); return v; } Texture* CreateProceduralCubeMap(int size, PixelFormat format, ProceduralTexEval func) { Image* faces[6]; for (int i = 0; i < 6; i++) { faces[i] = new Image(format, size, size); Image* face = faces[i]; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float s = ((float) x + 0.5f) / (float) size * 2 - 1; float t = ((float) y + 0.5f) / (float) size * 2 - 1; Vector3f v = cubeVector(i, s, t); func(v.x(), v.y(), v.z(), face->getPixelRow(y) + x * face->getComponents()); } } } Texture* tex = new CubeMap(faces); // Clean up the images for (int i = 0; i < 6; i++) { delete faces[i]; } return tex; } static Texture* CreateTextureFromImage(Image& img, Texture::AddressMode addressMode, Texture::MipMapMode mipMode) { Texture* tex = nullptr; const int maxDim = gl::maxTextureSize; if ((img.getWidth() > maxDim || img.getHeight() > maxDim)) { // The texture is too large; we need to split it. int uSplit = max(1, img.getWidth() / maxDim); int vSplit = max(1, img.getHeight() / maxDim); GetLogger()->info(_("Creating tiled texture. Width={}, max={}\n"), img.getWidth(), maxDim); tex = new TiledTexture(img, uSplit, vSplit, mipMode); } else { GetLogger()->info(_("Creating ordinary texture: {}x{}\n"), img.getWidth(), img.getHeight()); tex = new ImageTexture(img, addressMode, mipMode); } return tex; } Texture* LoadTextureFromFile(const fs::path& filename, Texture::AddressMode addressMode, Texture::MipMapMode mipMode) { // Check for a Celestia texture--these need to be handled specially. ContentType contentType = DetermineFileType(filename); if (contentType == Content_CelestiaTexture) return LoadVirtualTexture(filename); // All other texture types are handled by first loading an image, then // creating a texture from that image. Image* img = LoadImageFromFile(filename); if (img == nullptr) return nullptr; Texture* tex = CreateTextureFromImage(*img, addressMode, mipMode); if (contentType == Content_DXT5NormalMap) { // If the texture came from a .dxt5nm file then mark it as a dxt5 // compressed normal map. There's no separate OpenGL format for dxt5 // normal maps, so the file extension is the only thing that // distinguishes it from a plain old dxt5 texture. if (img->getFormat() == PixelFormat::DXT5) { tex->setFormatOptions(Texture::DXT5NormalMap); } } delete img; return tex; } // Load a height map texture from a file and convert it to a normal map. Texture* LoadHeightMapFromFile(const fs::path& filename, float height, Texture::AddressMode addressMode) { Image* img = LoadImageFromFile(filename); if (img == nullptr) return nullptr; Image* normalMap = img->computeNormalMap(height, addressMode == Texture::Wrap); delete img; if (normalMap == nullptr) return nullptr; Texture* tex = CreateTextureFromImage(*normalMap, addressMode, Texture::DefaultMipMaps); delete normalMap; return tex; }