Adds sRGB support for proper lighting

Currently we're incorrectly using sRGB colors as raw inputs for lighting
instead of conversion to linear space. For proper PBR we need linear
values.
This commit is contained in:
Vladislav Belov 2026-07-29 01:29:09 +02:00
parent e2f5b20922
commit f781181899
No known key found for this signature in database
GPG key ID: 353545E45DB9CCB3
14 changed files with 116 additions and 27 deletions

View file

@ -78,6 +78,9 @@
<optional>
<attribute name="define"/>
</optional>
<optional>
<attribute name="srgb"/>
</optional>
</element>
</zeroOrMore>
</interleave>

View file

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

View file

@ -53,6 +53,12 @@ public:
CTexturePtr Sampler;
};
struct RequiredSampler
{
CStrIntern name;
bool sRGB;
};
typedef std::vector<TextureSampler> 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<CStrIntern>& GetRequiredSampler() const { return m_RequiredSamplers; }
void AddRequiredSampler(RequiredSampler sampler);
const std::vector<RequiredSampler>& GetRequiredSamplers() const { return m_RequiredSamplers; }
private:
@ -92,7 +98,7 @@ private:
CTexturePtr m_DiffuseTexture;
SamplersVector m_Samplers;
std::vector<CStrIntern> m_RequiredSamplers;
std::vector<RequiredSampler> m_RequiredSamplers;
std::array<CStrIntern, static_cast<size_t>(Pass::COUNT)> m_ShaderEffects;
CShaderDefines m_ShaderDefines;

View file

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

View file

@ -51,6 +51,22 @@
#include <sstream>
#include <utility>
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<CObjectBase>& 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<const std::set<CStr>*>& 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<const std::set<CStr>*>& 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

View file

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

View file

@ -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};
};
/**

View file

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

View file

@ -352,11 +352,11 @@ std::unique_ptr<IDevice> 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;

View file

@ -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;
}

View file

@ -187,6 +187,9 @@ std::unique_ptr<CTexture> 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> 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:

View file

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

View file

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

View file

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