/* Copyright (C) 1999-2006 Id Software, Inc. and contributors. For a list of contributors, see the accompanying CONTRIBUTORS file. This file is part of GtkRadiant. GtkRadiant 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. GtkRadiant is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "textures.h" #include "debugging/debugging.h" #include "warnings.h" #include "itextures.h" #include "igl.h" #include "preferencesystem.h" #include "qgl.h" #include "texturelib.h" #include "container/hashfunc.h" #include "container/cache.h" #include "generic/callback.h" #include "stringio.h" #include "image.h" #include "texmanip.h" #include "preferences.h" enum ETexturesMode { eTextures_NEAREST = 0, eTextures_NEAREST_MIPMAP_NEAREST = 1, eTextures_NEAREST_MIPMAP_LINEAR = 2, eTextures_LINEAR = 3, eTextures_LINEAR_MIPMAP_NEAREST = 4, eTextures_LINEAR_MIPMAP_LINEAR = 5, eTextures_MAX_ANISOTROPY = 6, }; enum TextureCompressionFormat { TEXTURECOMPRESSION_NONE = 0, TEXTURECOMPRESSION_RGBA = 1, TEXTURECOMPRESSION_RGBA_S3TC_DXT1 = 2, TEXTURECOMPRESSION_RGBA_S3TC_DXT3 = 3, TEXTURECOMPRESSION_RGBA_S3TC_DXT5 = 4, }; struct texture_globals_t { // RIANT // texture compression format TextureCompressionFormat m_nTextureCompressionFormat; float fGamma; bool bTextureCompressionSupported; // is texture compression supported by hardware? GLint texture_components; // temporary values that should be initialised only once at run-time bool m_bOpenGLCompressionSupported; bool m_bS3CompressionSupported; texture_globals_t(GLint components) : m_nTextureCompressionFormat(TEXTURECOMPRESSION_NONE), fGamma(1.0f), bTextureCompressionSupported(false), texture_components(components), m_bOpenGLCompressionSupported(false), m_bS3CompressionSupported(false) { } }; texture_globals_t g_texture_globals(GL_RGBA); void SetTexParameters(ETexturesMode mode) { float maxAniso = QGL_maxTextureAnisotropy(); if (maxAniso > 1) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 1.0f); } else if (mode == eTextures_MAX_ANISOTROPY) { mode = eTextures_LINEAR_MIPMAP_LINEAR; } switch (mode) { case eTextures_NEAREST: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); break; case eTextures_NEAREST_MIPMAP_NEAREST: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); break; case eTextures_NEAREST_MIPMAP_LINEAR: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); break; case eTextures_LINEAR: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); break; case eTextures_LINEAR_MIPMAP_NEAREST: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); break; case eTextures_LINEAR_MIPMAP_LINEAR: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); break; case eTextures_MAX_ANISOTROPY: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso); break; default: globalOutputStream() << "invalid texture mode\n"; } } ETexturesMode g_texture_mode = eTextures_NEAREST_MIPMAP_NEAREST; byte g_gammatable[256]; void ResampleGamma(float fGamma) { int i, inf; if (fGamma == 1.0) { for (i = 0; i < 256; i++) { g_gammatable[i] = i; } } else { for (i = 0; i < 256; i++) { inf = (int) (255 * pow(static_cast((i + 0.5) / 255.5 ), static_cast( fGamma )) + 0.5); if (inf < 0) { inf = 0; } if (inf > 255) { inf = 255; } g_gammatable[i] = inf; } } } inline const int &min_int(const int &left, const int &right) { return std::min(left, right); } int max_tex_size = 0; const int max_texture_quality = 3; LatchedValue g_Textures_textureQuality(3, "Texture Quality"); /// \brief This function does the actual processing of raw RGBA data into a GL texture. /// It will also resample to power-of-two dimensions, generate the mipmaps and adjust gamma. void LoadTextureRGBA(qtexture_t *q, unsigned char *pPixels, int nWidth, int nHeight) { static float fGamma = -1; float total[3]; byte *outpixels = 0; int nCount = nWidth * nHeight; if (fGamma != g_texture_globals.fGamma) { fGamma = g_texture_globals.fGamma; ResampleGamma(fGamma); } q->width = nWidth; q->height = nHeight; total[0] = total[1] = total[2] = 0.0f; // resample texture gamma according to user settings for (int i = 0; i < (nCount * 4); i += 4) { for (int j = 0; j < 3; j++) { total[j] += (pPixels + i)[j]; byte b = (pPixels + i)[j]; (pPixels + i)[j] = g_gammatable[b]; } } q->color[0] = total[0] / (nCount * 255); q->color[1] = total[1] / (nCount * 255); q->color[2] = total[2] / (nCount * 255); glGenTextures(1, &q->texture_number); glBindTexture(GL_TEXTURE_2D, q->texture_number); SetTexParameters(g_texture_mode); int gl_width = 1; while (gl_width < nWidth) { gl_width <<= 1; } int gl_height = 1; while (gl_height < nHeight) { gl_height <<= 1; } bool resampled = false; if (!(gl_width == nWidth && gl_height == nHeight)) { resampled = true; outpixels = (byte *) malloc(gl_width * gl_height * 4); R_ResampleTexture(pPixels, nWidth, nHeight, outpixels, gl_width, gl_height, 4); } else { outpixels = pPixels; } int quality_reduction = max_texture_quality - g_Textures_textureQuality.m_value; int target_width = min_int(gl_width >> quality_reduction, max_tex_size); int target_height = min_int(gl_height >> quality_reduction, max_tex_size); while (gl_width > target_width || gl_height > target_height) { GL_MipReduce(outpixels, outpixels, gl_width, gl_height, target_width, target_height); if (gl_width > target_width) { gl_width >>= 1; } if (gl_height > target_height) { gl_height >>= 1; } } int mip = 0; glTexImage2D(GL_TEXTURE_2D, mip++, g_texture_globals.texture_components, gl_width, gl_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, outpixels); while (gl_width > 1 || gl_height > 1) { GL_MipReduce(outpixels, outpixels, gl_width, gl_height, 1, 1); if (gl_width > 1) { gl_width >>= 1; } if (gl_height > 1) { gl_height >>= 1; } glTexImage2D(GL_TEXTURE_2D, mip++, g_texture_globals.texture_components, gl_width, gl_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, outpixels); } glBindTexture(GL_TEXTURE_2D, 0); if (resampled) { free(outpixels); } } #if 0 /* ============== Texture_InitPalette ============== */ void Texture_InitPalette( byte *pal ){ int r,g,b; int i; int inf; byte gammatable[256]; float gamma; gamma = g_texture_globals.fGamma; if ( gamma == 1.0 ) { for ( i = 0 ; i < 256 ; i++ ) gammatable[i] = i; } else { for ( i = 0 ; i < 256 ; i++ ) { inf = (int)( 255 * pow( ( i + 0.5 ) / 255.5, gamma ) + 0.5 ); if ( inf < 0 ) { inf = 0; } if ( inf > 255 ) { inf = 255; } gammatable[i] = inf; } } for ( i = 0 ; i < 256 ; i++ ) { r = gammatable[pal[0]]; g = gammatable[pal[1]]; b = gammatable[pal[2]]; pal += 3; //v = (r<<24) + (g<<16) + (b<<8) + 255; //v = BigLong (v); //tex_palette[i] = v; tex_palette[i * 3 + 0] = r; tex_palette[i * 3 + 1] = g; tex_palette[i * 3 + 2] = b; } } #endif #if 0 class TestHashtable { public: TestHashtable(){ HashTable strings; strings["Monkey"] = "bleh"; strings["MonkeY"] = "blah"; } }; const TestHashtable g_testhashtable; #endif typedef std::pair TextureKey; void qtexture_realise(qtexture_t &texture, const TextureKey &key) { texture.texture_number = 0; if (!string_empty(key.second.c_str())) { Image *image = key.first.loadImage(key.second.c_str()); if (image != 0) { LoadTextureRGBA(&texture, image->getRGBAPixels(), image->getWidth(), image->getHeight()); texture.surfaceFlags = image->getSurfaceFlags(); texture.contentFlags = image->getContentFlags(); texture.value = image->getValue(); image->release(); globalOutputStream() << "Loaded Texture: \"" << key.second.c_str() << "\"\n"; GlobalOpenGL_debugAssertNoErrors(); } else { globalErrorStream() << "Texture load failed: \"" << key.second.c_str() << "\"\n"; } } } void qtexture_unrealise(qtexture_t &texture) { if (GlobalOpenGL().contextValid && texture.texture_number != 0) { glDeleteTextures(1, &texture.texture_number); GlobalOpenGL_debugAssertNoErrors(); } } class TextureKeyEqualNoCase { public: bool operator()(const TextureKey &key, const TextureKey &other) const { return key.first == other.first && string_equal_nocase(key.second.c_str(), other.second.c_str()); } }; class TextureKeyHashNoCase { public: typedef hash_t hash_type; hash_t operator()(const TextureKey &key) const { return hash_combine(string_hash_nocase(key.second.c_str()), pod_hash(key.first)); } }; #define DEBUG_TEXTURES 0 class TexturesMap : public TexturesCache { class TextureConstructor { TexturesMap *m_cache; public: explicit TextureConstructor(TexturesMap *cache) : m_cache(cache) { } qtexture_t *construct(const TextureKey &key) { qtexture_t *texture = new qtexture_t(key.first, key.second.c_str()); if (m_cache->realised()) { qtexture_realise(*texture, key); } return texture; } void destroy(qtexture_t *texture) { if (m_cache->realised()) { qtexture_unrealise(*texture); } delete texture; } }; typedef HashedCache qtextures_t; qtextures_t m_qtextures; TexturesCacheObserver *m_observer; std::size_t m_unrealised; public: virtual ~TexturesMap() = default; TexturesMap() : m_qtextures(TextureConstructor(this)), m_observer(0), m_unrealised(1) { } typedef qtextures_t::iterator iterator; iterator begin() { return m_qtextures.begin(); } iterator end() { return m_qtextures.end(); } LoadImageCallback defaultLoader() const { return LoadImageCallback(0, QERApp_LoadImage); } Image *loadImage(const char *name) { return defaultLoader().loadImage(name); } qtexture_t *capture(const char *name) { return capture(defaultLoader(), name); } qtexture_t *capture(const LoadImageCallback &loader, const char *name) { #if DEBUG_TEXTURES globalOutputStream() << "textures capture: " << makeQuoted( name ) << '\n'; #endif return m_qtextures.capture(TextureKey(loader, name)).get(); } void release(qtexture_t *texture) { #if DEBUG_TEXTURES globalOutputStream() << "textures release: " << makeQuoted( texture->name ) << '\n'; #endif m_qtextures.release(TextureKey(texture->load, texture->name)); } void attach(TexturesCacheObserver &observer) { ASSERT_MESSAGE(m_observer == 0, "TexturesMap::attach: cannot attach observer"); m_observer = &observer; } void detach(TexturesCacheObserver &observer) { ASSERT_MESSAGE(m_observer == &observer, "TexturesMap::detach: cannot detach observer"); m_observer = 0; } void realise() { if (--m_unrealised == 0) { g_texture_globals.bTextureCompressionSupported = false; if (GlobalOpenGL().ARB_texture_compression()) { g_texture_globals.bTextureCompressionSupported = true; g_texture_globals.m_bOpenGLCompressionSupported = true; } if (GlobalOpenGL().EXT_texture_compression_s3tc()) { g_texture_globals.bTextureCompressionSupported = true; g_texture_globals.m_bS3CompressionSupported = true; } switch (g_texture_globals.texture_components) { case GL_RGBA: break; case GL_COMPRESSED_RGBA_ARB: if (!g_texture_globals.m_bOpenGLCompressionSupported) { globalOutputStream() << "OpenGL extension GL_ARB_texture_compression not supported by current graphics drivers\n"; g_texture_globals.m_nTextureCompressionFormat = TEXTURECOMPRESSION_NONE; g_texture_globals.texture_components = GL_RGBA; } break; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: if (!g_texture_globals.m_bS3CompressionSupported) { globalOutputStream() << "OpenGL extension GL_EXT_texture_compression_s3tc not supported by current graphics drivers\n"; if (g_texture_globals.m_bOpenGLCompressionSupported) { g_texture_globals.m_nTextureCompressionFormat = TEXTURECOMPRESSION_RGBA; g_texture_globals.texture_components = GL_COMPRESSED_RGBA_ARB; } else { g_texture_globals.m_nTextureCompressionFormat = TEXTURECOMPRESSION_NONE; g_texture_globals.texture_components = GL_RGBA; } } break; default: globalOutputStream() << "Unknown texture compression selected, reverting\n"; g_texture_globals.m_nTextureCompressionFormat = TEXTURECOMPRESSION_NONE; g_texture_globals.texture_components = GL_RGBA; break; } glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_tex_size); if (max_tex_size == 0) { max_tex_size = 1024; } for (qtextures_t::iterator i = m_qtextures.begin(); i != m_qtextures.end(); ++i) { if (!(*i).value.empty()) { qtexture_realise(*(*i).value, (*i).key); } } if (m_observer != 0) { m_observer->realise(); } } } void unrealise() { if (++m_unrealised == 1) { if (m_observer != 0) { m_observer->unrealise(); } for (qtextures_t::iterator i = m_qtextures.begin(); i != m_qtextures.end(); ++i) { if (!(*i).value.empty()) { qtexture_unrealise(*(*i).value); } } } } bool realised() { return m_unrealised == 0; } }; TexturesMap *g_texturesmap; TexturesCache &GetTexturesCache() { return *g_texturesmap; } void Textures_Realise() { g_texturesmap->realise(); } void Textures_Unrealise() { g_texturesmap->unrealise(); } Callback g_texturesModeChangedNotify; void Textures_setModeChangedNotify(const Callback ¬ify) { g_texturesModeChangedNotify = notify; } void Textures_ModeChanged() { if (g_texturesmap->realised()) { SetTexParameters(g_texture_mode); for (TexturesMap::iterator i = g_texturesmap->begin(); i != g_texturesmap->end(); ++i) { glBindTexture(GL_TEXTURE_2D, (*i).value->texture_number); SetTexParameters(g_texture_mode); } glBindTexture(GL_TEXTURE_2D, 0); } g_texturesModeChangedNotify(); } void Textures_SetMode(ETexturesMode mode) { if (g_texture_mode != mode) { g_texture_mode = mode; Textures_ModeChanged(); } } void Textures_setTextureComponents(GLint texture_components) { if (g_texture_globals.texture_components != texture_components) { Textures_Unrealise(); g_texture_globals.texture_components = texture_components; Textures_Realise(); } } void Textures_UpdateTextureCompressionFormat() { GLint texture_components = GL_RGBA; switch (g_texture_globals.m_nTextureCompressionFormat) { case (TEXTURECOMPRESSION_NONE): { texture_components = GL_RGBA; break; } case (TEXTURECOMPRESSION_RGBA): { texture_components = GL_COMPRESSED_RGBA_ARB; break; } case (TEXTURECOMPRESSION_RGBA_S3TC_DXT1): { texture_components = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; break; } case (TEXTURECOMPRESSION_RGBA_S3TC_DXT3): { texture_components = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; } case (TEXTURECOMPRESSION_RGBA_S3TC_DXT5): { texture_components = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; } } Textures_setTextureComponents(texture_components); } struct TextureCompression { static void Export(const TextureCompressionFormat &self, const Callback &returnz) { returnz(self); } static void Import(TextureCompressionFormat &self, int value) { if (!g_texture_globals.m_bOpenGLCompressionSupported && g_texture_globals.m_bS3CompressionSupported && value >= 1) { ++value; } switch (value) { case 0: self = TEXTURECOMPRESSION_NONE; break; case 1: self = TEXTURECOMPRESSION_RGBA; break; case 2: self = TEXTURECOMPRESSION_RGBA_S3TC_DXT1; break; case 3: self = TEXTURECOMPRESSION_RGBA_S3TC_DXT3; break; case 4: self = TEXTURECOMPRESSION_RGBA_S3TC_DXT5; break; } Textures_UpdateTextureCompressionFormat(); } }; struct TextureGamma { static void Export(const float &self, const Callback &returnz) { returnz(self); } static void Import(float &self, float value) { if (value != self) { Textures_Unrealise(); self = value; Textures_Realise(); } } }; struct TextureMode { static void Export(const ETexturesMode &self, const Callback &returnz) { switch (self) { case eTextures_NEAREST: returnz(0); break; case eTextures_NEAREST_MIPMAP_NEAREST: returnz(1); break; case eTextures_LINEAR: returnz(2); break; case eTextures_NEAREST_MIPMAP_LINEAR: returnz(3); break; case eTextures_LINEAR_MIPMAP_NEAREST: returnz(4); break; case eTextures_LINEAR_MIPMAP_LINEAR: returnz(5); break; case eTextures_MAX_ANISOTROPY: returnz(6); break; default: returnz(4); } } static void Import(ETexturesMode &self, int value) { switch (value) { case 0: Textures_SetMode(eTextures_NEAREST); break; case 1: Textures_SetMode(eTextures_NEAREST_MIPMAP_NEAREST); break; case 2: Textures_SetMode(eTextures_LINEAR); break; case 3: Textures_SetMode(eTextures_NEAREST_MIPMAP_LINEAR); break; case 4: Textures_SetMode(eTextures_LINEAR_MIPMAP_NEAREST); break; case 5: Textures_SetMode(eTextures_LINEAR_MIPMAP_LINEAR); break; case 6: Textures_SetMode(eTextures_MAX_ANISOTROPY); } } }; void Textures_constructPreferences(PreferencesPage &page) { { const char *percentages[] = {"12.5%", "25%", "50%", "100%",}; page.appendRadio( "Texture Quality", STRING_ARRAY_RANGE(percentages), make_property(g_Textures_textureQuality) ); } page.appendSpinner( "Texture Gamma", 1.0, 0.0, 1.0, make_property(g_texture_globals.fGamma) ); { const char *texture_mode[] = {"Nearest", "Nearest Mipmap", "Linear", "Bilinear", "Bilinear Mipmap", "Trilinear", "Anisotropy"}; page.appendCombo( "Texture Render Mode", STRING_ARRAY_RANGE(texture_mode), make_property(g_texture_mode) ); } { const char *compression_none[] = {"None"}; const char *compression_opengl[] = {"None", "OpenGL ARB"}; const char *compression_s3tc[] = {"None", "S3TC DXT1", "S3TC DXT3", "S3TC DXT5"}; const char *compression_opengl_s3tc[] = {"None", "OpenGL ARB", "S3TC DXT1", "S3TC DXT3", "S3TC DXT5"}; StringArrayRange compression( (g_texture_globals.m_bOpenGLCompressionSupported) ? (g_texture_globals.m_bS3CompressionSupported) ? STRING_ARRAY_RANGE(compression_opengl_s3tc) : STRING_ARRAY_RANGE(compression_opengl) : (g_texture_globals.m_bS3CompressionSupported) ? STRING_ARRAY_RANGE(compression_s3tc) : STRING_ARRAY_RANGE(compression_none) ); page.appendCombo( "Hardware Texture Compression", compression, make_property(g_texture_globals.m_nTextureCompressionFormat) ); } } void Textures_constructPage(PreferenceGroup &group) { PreferencesPage page(group.createPage("Textures", "Texture Settings")); Textures_constructPreferences(page); } void Textures_registerPreferencesPage() { PreferencesDialog_addDisplayPage(makeCallbackF(Textures_constructPage)); } struct TextureCompressionPreference { static void Export(const Callback &returnz) { returnz(g_texture_globals.m_nTextureCompressionFormat); } static void Import(int value) { g_texture_globals.m_nTextureCompressionFormat = static_cast( value ); Textures_UpdateTextureCompressionFormat(); } }; void Textures_Construct() { g_texturesmap = new TexturesMap; GlobalPreferenceSystem().registerPreference("TextureCompressionFormat", make_property_string()); GlobalPreferenceSystem().registerPreference("TextureFiltering", make_property_string(reinterpret_cast( g_texture_mode ))); GlobalPreferenceSystem().registerPreference("TextureQuality", make_property_string(g_Textures_textureQuality.m_latched)); GlobalPreferenceSystem().registerPreference("SI_Gamma", make_property_string(g_texture_globals.fGamma)); g_Textures_textureQuality.useLatched(); Textures_registerPreferencesPage(); Textures_ModeChanged(); } void Textures_Destroy() { delete g_texturesmap; } #include "modulesystem/modulesmap.h" #include "modulesystem/singletonmodule.h" #include "modulesystem/moduleregistry.h" class TexturesDependencies : public GlobalRadiantModuleRef, public GlobalOpenGLModuleRef, public GlobalPreferenceSystemModuleRef { ImageModulesRef m_image_modules; public: TexturesDependencies() : m_image_modules(GlobalRadiant().getRequiredGameDescriptionKeyValue("texturetypes")) { } ImageModules &getImageModules() { return m_image_modules.get(); } }; class TexturesAPI { TexturesCache *m_textures; public: typedef TexturesCache Type; STRING_CONSTANT(Name, "*"); TexturesAPI() { Textures_Construct(); m_textures = &GetTexturesCache(); } ~TexturesAPI() { Textures_Destroy(); } TexturesCache *getTable() { return m_textures; } }; typedef SingletonModule TexturesModule; typedef Static StaticTexturesModule; StaticRegisterModule staticRegisterTextures(StaticTexturesModule::instance()); ImageModules &Textures_getImageModules() { return StaticTexturesModule::instance().getDependencies().getImageModules(); }