diff --git a/binaries/data/mods/mod/art/materials/material.rng b/binaries/data/mods/mod/art/materials/material.rng index e92f863452..6fb86260bb 100644 --- a/binaries/data/mods/mod/art/materials/material.rng +++ b/binaries/data/mods/mod/art/materials/material.rng @@ -78,6 +78,9 @@ + + + diff --git a/source/graphics/Material.cpp b/source/graphics/Material.cpp index e361c8391b..f52adec03f 100644 --- a/source/graphics/Material.cpp +++ b/source/graphics/Material.cpp @@ -62,8 +62,7 @@ void CMaterial::AddRenderQuery(const char* key) m_RenderQueries.Add(key); } -void CMaterial::AddRequiredSampler(const CStr& samplerName) +void CMaterial::AddRequiredSampler(RequiredSampler sampler) { - CStrIntern string(samplerName); - m_RequiredSamplers.push_back(string); + m_RequiredSamplers.emplace_back(std::move(sampler)); } diff --git a/source/graphics/Material.h b/source/graphics/Material.h index efe557a0e2..f326bc2902 100644 --- a/source/graphics/Material.h +++ b/source/graphics/Material.h @@ -53,6 +53,12 @@ public: CTexturePtr Sampler; }; + struct RequiredSampler + { + CStrIntern name; + bool sRGB; + }; + typedef std::vector SamplersVector; CMaterial(); @@ -82,8 +88,8 @@ public: void AddRenderQuery(const char* key); const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; } - void AddRequiredSampler(const CStr& samplerName); - const std::vector& GetRequiredSampler() const { return m_RequiredSamplers; } + void AddRequiredSampler(RequiredSampler sampler); + const std::vector& GetRequiredSamplers() const { return m_RequiredSamplers; } private: @@ -92,7 +98,7 @@ private: CTexturePtr m_DiffuseTexture; SamplersVector m_Samplers; - std::vector m_RequiredSamplers; + std::vector m_RequiredSamplers; std::array(Pass::COUNT)> m_ShaderEffects; CShaderDefines m_ShaderDefines; diff --git a/source/graphics/MaterialManager.cpp b/source/graphics/MaterialManager.cpp index e27d70182f..075e741a94 100644 --- a/source/graphics/MaterialManager.cpp +++ b/source/graphics/MaterialManager.cpp @@ -103,6 +103,7 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname) AT(material); AT(name); AT(pass); + AT(srgb); AT(value); #undef AT #undef EL @@ -161,7 +162,8 @@ CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname) } else if (token == el_required_texture) { - material.AddRequiredSampler(attrs.GetNamedItem(at_name)); + const bool sRGB{attrs.GetNamedItem(at_srgb) == "true"}; + material.AddRequiredSampler({CStrIntern{attrs.GetNamedItem(at_name)}, sRGB}); if (!attrs.GetNamedItem(at_define).empty()) material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1); } diff --git a/source/graphics/ObjectEntry.cpp b/source/graphics/ObjectEntry.cpp index 40272cfa58..518a3e1199 100644 --- a/source/graphics/ObjectEntry.cpp +++ b/source/graphics/ObjectEntry.cpp @@ -51,6 +51,22 @@ #include #include +namespace +{ + +bool ShouldInterpretTextureAsSRGB(const CObjectBase::Samp& sampler, const CMaterial& material) +{ + const auto requiredSamplers{material.GetRequiredSamplers()}; + auto it{std::find_if(requiredSamplers.begin(), requiredSamplers.end(), + [&](const CMaterial::RequiredSampler& requiredSampler) + { + return requiredSampler.name == sampler.m_SamplerName; + })}; + return it != requiredSamplers.end() && it->sRGB; +} + +} + CObjectEntry::CObjectEntry(const std::shared_ptr& base, const CSimulation2& simulation) : m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation) { @@ -155,6 +171,7 @@ bool CObjectEntry::BuildVariation(const std::vector*>& comp { CTextureProperties textureProps(samp.m_SamplerFile); textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); + textureProps.SetSRGB(ShouldInterpretTextureAsSRGB(samp, material)); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); // if we've loaded this model we're probably going to render it soon, so prefetch its texture. // All textures are prefetched even in the fixed pipeline, including the normal maps etc. @@ -167,11 +184,14 @@ bool CObjectEntry::BuildVariation(const std::vector*>& comp CModel* model = newModel.get(); m_Model = std::move(newModel); - for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler()) + for (const auto& requiredSampler : model->GetMaterial().GetRequiredSamplers()) { if (std::find_if(m_Samplers.begin(), m_Samplers.end(), - [&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end()) - LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str()); + [&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requiredSampler.name; }) == m_Samplers.end()) + { + LOGERROR("Actor %s: required texture sampler %s not found (material %s)", + m_Base->GetIdentifier(), requiredSampler.name.string().c_str(), m_Base->m_Material.string8().c_str()); + } } // calculate initial object space bounds, based on vertex positions diff --git a/source/graphics/TextureManager.cpp b/source/graphics/TextureManager.cpp index f3afdd1ae1..86ab0bd9f0 100644 --- a/source/graphics/TextureManager.cpp +++ b/source/graphics/TextureManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -60,7 +60,7 @@ namespace { Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded( - Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC) + Renderer::Backend::IDevice* device, Tex& textureData, const bool hasS3TC, const bool sRGB) { const bool alpha = (textureData.m_Flags & TEX_ALPHA) != 0; const bool grey = (textureData.m_Flags & TEX_GREY) != 0; @@ -81,13 +81,13 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded( switch (dxt) { case DXT1A: - return Renderer::Backend::Format::BC1_RGBA_UNORM; + return sRGB ? Renderer::Backend::Format::BC1_RGBA_SRGB : Renderer::Backend::Format::BC1_RGBA_UNORM; case 1: - return Renderer::Backend::Format::BC1_RGB_UNORM; + return sRGB ? Renderer::Backend::Format::BC1_RGB_SRGB : Renderer::Backend::Format::BC1_RGB_UNORM; case 3: - return Renderer::Backend::Format::BC2_UNORM; + return sRGB ? Renderer::Backend::Format::BC2_SRGB : Renderer::Backend::Format::BC2_UNORM; case 5: - return Renderer::Backend::Format::BC3_UNORM; + return sRGB ? Renderer::Backend::Format::BC3_SRGB : Renderer::Backend::Format::BC3_UNORM; default: LOGERROR("Unknown DXT compression."); return Renderer::Backend::Format::UNDEFINED; @@ -101,9 +101,12 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded( { case 8: ENSURE(grey); + ENSURE(!sRGB); return Renderer::Backend::Format::L8_UNORM; case 24: + ENSURE(grey); ENSURE(!alpha); + ENSURE(!sRGB); if (device->IsTextureFormatSupported(Renderer::Backend::Format::R8G8B8_UNORM)) return Renderer::Backend::Format::R8G8B8_UNORM; else @@ -114,7 +117,7 @@ Renderer::Backend::Format ChooseFormatAndTransformTextureDataIfNeeded( } case 32: ENSURE(alpha); - return Renderer::Backend::Format::R8G8B8A8_UNORM; + return sRGB ? Renderer::Backend::Format::R8G8B8A8_SRGB : Renderer::Backend::Format::R8G8B8A8_UNORM; default: LOGERROR("Unsupported BPP: %zu", textureData.m_Bpp); } @@ -336,6 +339,7 @@ struct TPhash hash_combine(seed, textureProperties.m_AnisotropicFilterEnabled); hash_combine(seed, textureProperties.m_FormatOverride); hash_combine(seed, textureProperties.m_IgnoreQuality); + hash_combine(seed, textureProperties.m_SRGB); return seed; } @@ -358,7 +362,8 @@ struct TPequal_to lhs.m_AddressModeV == rhs.m_AddressModeV && lhs.m_AnisotropicFilterEnabled == rhs.m_AnisotropicFilterEnabled && lhs.m_FormatOverride == rhs.m_FormatOverride && - lhs.m_IgnoreQuality == rhs.m_IgnoreQuality; + lhs.m_IgnoreQuality == rhs.m_IgnoreQuality && + lhs.m_SRGB == rhs.m_SRGB; } bool operator()(const CTexturePtr& lhs, const CTexturePtr& rhs) const @@ -526,7 +531,8 @@ public: } else { - format = ChooseFormatAndTransformTextureDataIfNeeded(m_Device, textureData, m_HasS3TC); + format = ChooseFormatAndTransformTextureDataIfNeeded( + m_Device, textureData, m_HasS3TC, texture->m_Properties.m_SRGB); } if (format == Renderer::Backend::Format::UNDEFINED) diff --git a/source/graphics/TextureManager.h b/source/graphics/TextureManager.h index 16289a3e0f..4320a00030 100644 --- a/source/graphics/TextureManager.h +++ b/source/graphics/TextureManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -245,6 +245,8 @@ public: void SetIgnoreQuality(bool ignore) { m_IgnoreQuality = ignore; } + void SetSRGB(const bool sRGB) { m_SRGB = sRGB; } + private: // Must update TPhash, TPequal_to when changing these fields VfsPath m_Path; @@ -257,6 +259,7 @@ private: Renderer::Backend::Format m_FormatOverride = Renderer::Backend::Format::UNDEFINED; bool m_IgnoreQuality = false; + bool m_SRGB{false}; }; /** diff --git a/source/renderer/backend/Format.h b/source/renderer/backend/Format.h index b8b9cb3aed..6949fb7240 100644 --- a/source/renderer/backend/Format.h +++ b/source/renderer/backend/Format.h @@ -32,8 +32,10 @@ enum class Format R8G8_UINT, R8G8B8_UNORM, R8G8B8A8_UNORM, + R8G8B8A8_SRGB, R8G8B8A8_UINT, B8G8R8A8_UNORM, + B8G8R8A8_SRGB, // TODO: we need to drop legacy A8 and L8 formats as soon as we have proper // channel swizzling. @@ -65,9 +67,13 @@ enum class Format D32_SFLOAT_S8_UINT, BC1_RGB_UNORM, + BC1_RGB_SRGB, BC1_RGBA_UNORM, + BC1_RGBA_SRGB, BC2_UNORM, - BC3_UNORM + BC2_SRGB, + BC3_UNORM, + BC3_SRGB, }; inline bool IsDepthFormat(const Format format) diff --git a/source/renderer/backend/gl/Device.cpp b/source/renderer/backend/gl/Device.cpp index 4edcfecebe..7472e9473d 100644 --- a/source/renderer/backend/gl/Device.cpp +++ b/source/renderer/backend/gl/Device.cpp @@ -352,11 +352,11 @@ std::unique_ptr CDevice::Create(SDL_Window* window) #if CONFIG2_GLES // Some GLES implementations have GL_EXT_texture_compression_dxt1 // but that only supports DXT1 so we can't use it. - capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc; + capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_compression_s3tc_srgb; #else // Note: we don't bother checking for GL_S3_s3tc - it is incompatible // and irrelevant (was never widespread). - capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc; + capabilities.S3TC = GLAD_GL_EXT_texture_compression_s3tc && GLAD_GL_EXT_texture_sRGB; #endif #if CONFIG2_GLES capabilities.multisampling = false; @@ -738,6 +738,7 @@ bool CDevice::IsTextureFormatSupported(const Format format) const case Format::R8G8B8_UNORM: case Format::R8G8B8A8_UNORM: + case Format::R8G8B8A8_SRGB: case Format::A8_UNORM: case Format::L8_UNORM: supported = true; @@ -763,9 +764,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const break; case Format::BC1_RGB_UNORM: + case Format::BC1_RGB_SRGB: case Format::BC1_RGBA_UNORM: + case Format::BC1_RGBA_SRGB: case Format::BC2_UNORM: + case Format::BC2_SRGB: case Format::BC3_UNORM: + case Format::BC3_SRGB: supported = m_Capabilities.S3TC; break; diff --git a/source/renderer/backend/gl/DeviceCommandContext.cpp b/source/renderer/backend/gl/DeviceCommandContext.cpp index 098f3c5ed8..cb065505a7 100644 --- a/source/renderer/backend/gl/DeviceCommandContext.cpp +++ b/source/renderer/backend/gl/DeviceCommandContext.cpp @@ -348,7 +348,8 @@ void CDeviceCommandContext::UploadTextureRegion( GLenum pixelFormat = GL_RGBA; switch (dataFormat) { - case Format::R8G8B8A8_UNORM: + case Format::R8G8B8A8_UNORM: [[fallthrough]]; + case Format::R8G8B8A8_SRGB: break; case Format::R8G8B8_UNORM: pixelFormat = GL_RGB; @@ -381,9 +382,13 @@ void CDeviceCommandContext::UploadTextureRegion( } else if ( texture->GetFormat() == Format::BC1_RGB_UNORM || + texture->GetFormat() == Format::BC1_RGB_SRGB || texture->GetFormat() == Format::BC1_RGBA_UNORM || + texture->GetFormat() == Format::BC1_RGBA_SRGB || texture->GetFormat() == Format::BC2_UNORM || - texture->GetFormat() == Format::BC3_UNORM) + texture->GetFormat() == Format::BC2_SRGB || + texture->GetFormat() == Format::BC3_UNORM || + texture->GetFormat() == Format::BC3_SRGB) { ENSURE(xOffset == 0 && yOffset == 0); ENSURE(texture->GetFormat() == dataFormat); @@ -392,15 +397,27 @@ void CDeviceCommandContext::UploadTextureRegion( GLenum internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT; switch (texture->GetFormat()) { + case Format::BC1_RGB_SRGB: + internalFormat = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; + break; case Format::BC1_RGBA_UNORM: internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; break; + case Format::BC1_RGBA_SRGB: + internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + break; case Format::BC2_UNORM: internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; + case Format::BC2_SRGB: + internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT; + break; case Format::BC3_UNORM: internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; + case Format::BC3_SRGB: + internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; + break; default: break; } diff --git a/source/renderer/backend/gl/Texture.cpp b/source/renderer/backend/gl/Texture.cpp index e166a1871f..219b8efd13 100644 --- a/source/renderer/backend/gl/Texture.cpp +++ b/source/renderer/backend/gl/Texture.cpp @@ -187,6 +187,9 @@ std::unique_ptr CTexture::Create( break; case Format::R8G8B8A8_UNORM: break; + case Format::R8G8B8A8_SRGB: + internalFormat = GL_SRGB_ALPHA_EXT; + break; case Format::R8G8B8_UNORM: internalFormat = GL_RGB; pixelFormat = GL_RGB; @@ -254,9 +257,13 @@ std::unique_ptr CTexture::Create( break; #endif case Format::BC1_RGB_UNORM: + case Format::BC1_RGB_SRGB: case Format::BC1_RGBA_UNORM: + case Format::BC1_RGBA_SRGB: case Format::BC2_UNORM: + case Format::BC2_SRGB: case Format::BC3_UNORM: + case Format::BC3_SRGB: compressedFormat = true; break; default: diff --git a/source/renderer/backend/vulkan/Device.cpp b/source/renderer/backend/vulkan/Device.cpp index a78c17ccd8..fa0c8808ac 100644 --- a/source/renderer/backend/vulkan/Device.cpp +++ b/source/renderer/backend/vulkan/Device.cpp @@ -847,9 +847,13 @@ bool CDevice::IsTextureFormatSupported(const Format format) const return false; case Format::BC1_RGB_UNORM: + case Format::BC1_RGB_SRGB: case Format::BC1_RGBA_UNORM: + case Format::BC1_RGBA_SRGB: case Format::BC2_UNORM: + case Format::BC2_SRGB: case Format::BC3_UNORM: + case Format::BC3_SRGB: if (m_Capabilities.S3TC) return true; else diff --git a/source/renderer/backend/vulkan/Mapping.cpp b/source/renderer/backend/vulkan/Mapping.cpp index 06843111a7..133c7ced97 100644 --- a/source/renderer/backend/vulkan/Mapping.cpp +++ b/source/renderer/backend/vulkan/Mapping.cpp @@ -174,8 +174,10 @@ VkFormat FromFormat(const Format format) CASE(R8G8_UNORM) CASE(R8G8_UINT) CASE(R8G8B8A8_UNORM) + CASE(R8G8B8A8_SRGB) CASE(R8G8B8A8_UINT) CASE(B8G8R8A8_UNORM) + CASE(B8G8R8A8_SRGB) CASE(R16_UNORM) CASE(R16_UINT) @@ -204,9 +206,13 @@ VkFormat FromFormat(const Format format) CASE(D32_SFLOAT) CASE2(BC1_RGB_UNORM, BC1_RGB_UNORM_BLOCK) + CASE2(BC1_RGB_SRGB, BC1_RGB_SRGB_BLOCK) CASE2(BC1_RGBA_UNORM, BC1_RGBA_UNORM_BLOCK) + CASE2(BC1_RGBA_SRGB, BC1_RGBA_SRGB_BLOCK) CASE2(BC2_UNORM, BC2_UNORM_BLOCK) + CASE2(BC2_SRGB, BC2_SRGB_BLOCK) CASE2(BC3_UNORM, BC3_UNORM_BLOCK) + CASE2(BC3_SRGB, BC3_SRGB_BLOCK) #undef CASE #undef CASE2 diff --git a/source/renderer/backend/vulkan/RingCommandContext.cpp b/source/renderer/backend/vulkan/RingCommandContext.cpp index 3dec15b729..403f865c8c 100644 --- a/source/renderer/backend/vulkan/RingCommandContext.cpp +++ b/source/renderer/backend/vulkan/RingCommandContext.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -179,13 +179,18 @@ void CRingCommandContext::ScheduleUpload( const bool isCompressedFormat = format == Format::BC1_RGB_UNORM || + format == Format::BC1_RGB_SRGB || format == Format::BC1_RGBA_UNORM || + format == Format::BC1_RGBA_SRGB || format == Format::BC2_UNORM || - format == Format::BC3_UNORM; + format == Format::BC2_SRGB || + format == Format::BC3_UNORM || + format == Format::BC3_SRGB; ENSURE( format == Format::R8_UNORM || format == Format::R8G8_UNORM || format == Format::R8G8B8A8_UNORM || + format == Format::R8G8B8A8_SRGB || format == Format::A8_UNORM || format == Format::L8_UNORM || isCompressedFormat);