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