From 81f2e5509d5e7b7045b7b3c4fe03b0d674a99c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ptaszek?= Date: Thu, 17 Sep 2026 22:10:09 +0200 Subject: [PATCH] Models: per-vertex color from glTF COLOR_0 (#3374) * Add a per-vertex RGBA8 color to the models VBO SVertexData gets a normalized RGBA8 color (76 -> 80 bytes), bound at attribute 15 of the models VAO and mirrored as the `color` attribute of VBO:ModelsVBO(). It defaults to opaque white, the glTF meaning of an absent COLOR_0, and nothing fills it yet. Attribute 15 is the last one OpenGL guarantees; Lua VAOs built on the models VBO add their instance attributes from 6 upwards. * Load glTF COLOR_0 into the model vertex color The glTF parser reads COLOR_0 (VEC3 or VEC4, float or normalized integer) into the new vertex color, clamped to 0..1 and quantized to bytes; VEC3 gets opaque alpha. Models without it keep the white default. Engine.FeatureSupport.modelVertexColor tells Lua the color is available. * Add changelog entry --- doc/pr-changelogs/3374.md | 5 +++++ rts/Lua/LuaConstEngine.cpp | 2 ++ rts/Lua/LuaVBOImpl.cpp | 14 +++++++++++++- rts/Rendering/Models/3DModelVAO.cpp | 7 +++++++ rts/Rendering/Models/GLTFParser.cpp | 12 ++++++++++++ rts/Rendering/Models/VertexData.cpp | 2 +- rts/Rendering/Models/VertexData.hpp | 5 +++++ 7 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 doc/pr-changelogs/3374.md diff --git a/doc/pr-changelogs/3374.md b/doc/pr-changelogs/3374.md new file mode 100644 index 00000000000..c4c5b5785c8 --- /dev/null +++ b/doc/pr-changelogs/3374.md @@ -0,0 +1,5 @@ +### Features + * glTF models now load the `COLOR_0` vertex attribute. Model shaders can read it as `layout (location = 15) in vec4 color`, and Lua as the `color` attribute of `VBO:ModelsVBO()`. Models without it (including all S3O, 3DO and assimp models) read opaque white. `Engine.FeatureSupport.modelVertexColor` is `true` on engines that have it. + +### Caveats + * one element of `VBO:ModelsVBO()` is now 80 bytes instead of 76; Lua code that hardcoded the old stride needs updating. diff --git a/rts/Lua/LuaConstEngine.cpp b/rts/Lua/LuaConstEngine.cpp index 2469286bf9d..1c57bc5055b 100644 --- a/rts/Lua/LuaConstEngine.cpp +++ b/rts/Lua/LuaConstEngine.cpp @@ -31,6 +31,7 @@ * @field reliableLuaMapShaders boolean Whether forward-only Lua map shaders activate without a deferred draw and Spring.SetMapShader program swaps refresh cached uniform locations * @field nanoParticleUpdateCallin boolean Whether LuaUI receives batched `NanoParticleUpdate` lifecycle events * @field nanoParticlesGL4 boolean Whether the engine has the standalone shader-based nano particle effect (the `NanoParticles*` springsettings) + * @field modelVertexColor boolean Whether glTF `COLOR_0` is loaded into the models VBO (`color`, attribute 15; opaque white when absent) */ /*** @@ -89,6 +90,7 @@ bool LuaConstEngine::PushEntries(lua_State* L) LuaPushNamedBool(L, "reliableLuaMapShaders", true); LuaPushNamedBool(L, "nanoParticleUpdateCallin", true); LuaPushNamedBool(L, "nanoParticlesGL4", true); + LuaPushNamedBool(L, "modelVertexColor", true); lua_rawset(L, -3); lua_pushliteral(L, "textColorCodes"); diff --git a/rts/Lua/LuaVBOImpl.cpp b/rts/Lua/LuaVBOImpl.cpp index 0cf3ad038d1..7e2b59f83d0 100644 --- a/rts/Lua/LuaVBOImpl.cpp +++ b/rts/Lua/LuaVBOImpl.cpp @@ -814,6 +814,7 @@ void LuaVBOImpl::UpdateModelsVBOElementCount() // support an arbitrary number of channels, would be easy but overkill (for now) float2 texCoords[NUM_MODEL_UVCHANNS]; uvec3 in uvec3 bonesInfo; + u8vec4 color; */ size_t LuaVBOImpl::ModelsVBOImpl() { @@ -884,7 +885,18 @@ size_t LuaVBOImpl::ModelsVBOImpl() 3 * sizeof(uint32_t) //strideSizeInBytes }; - this->attributesCount = 6; + // uint8_t color[4] + this->bufferAttribDefs[15] = { + GL_UNSIGNED_BYTE, //type + 4, //size + GL_TRUE, //normalized + "color", //name + offsetof(SVertexData, color), //pointer + sizeof(uint8_t), //typeSizeInBytes + 4 * sizeof(uint8_t) //strideSizeInBytes + }; + + this->attributesCount = 7; this->elemSizeInBytes = sizeof(SVertexData); this->bufferSizeInBytes = vbo->GetSize(); this->elementsCount = S3DModelVAO::GetInstance().GetVertElemCount(); diff --git a/rts/Rendering/Models/3DModelVAO.cpp b/rts/Rendering/Models/3DModelVAO.cpp index ae9d82d799c..94cc62e1dda 100644 --- a/rts/Rendering/Models/3DModelVAO.cpp +++ b/rts/Rendering/Models/3DModelVAO.cpp @@ -31,6 +31,11 @@ void S3DModelVAO::EnableAttribs(bool inst) const glVertexAttribPointer (3, 3, GL_FLOAT , false, sizeof(SVertexData), (const void*)offsetof(SVertexData, tTangent )); glVertexAttribPointer (4, 4, GL_FLOAT , false, sizeof(SVertexData), (const void*)offsetof(SVertexData, texCoords[0])); glVertexAttribIPointer(5, 3, GL_UNSIGNED_INT, sizeof(SVertexData), (const void*)offsetof(SVertexData, boneIDsLow )); + + // last guaranteed slot, Lua VAOs on this buffer put their instance attribs from 6 up + glEnableVertexAttribArray(15); + glVertexAttribDivisor(15, 0); + glVertexAttribPointer (15, 4, GL_UNSIGNED_BYTE, true , sizeof(SVertexData), (const void*)offsetof(SVertexData, color )); } else { for (int i = 6; i <= 6; ++i) { @@ -50,6 +55,8 @@ void S3DModelVAO::DisableAttribs() const glDisableVertexAttribArray(i); glVertexAttribDivisor(i, 0); } + + glDisableVertexAttribArray(15); } S3DModelVAO::S3DModelVAO() diff --git a/rts/Rendering/Models/GLTFParser.cpp b/rts/Rendering/Models/GLTFParser.cpp index 119e1421860..5eebc3abcbf 100644 --- a/rts/Rendering/Models/GLTFParser.cpp +++ b/rts/Rendering/Models/GLTFParser.cpp @@ -131,6 +131,18 @@ namespace Impl { }); seenTangents = true; } break; + case hashString("COLOR_0"): { + const auto ToByte = [](float v) { return static_cast(std::clamp(v, 0.0f, 1.0f) * 255.0f + 0.5f); }; + if (accessor.type == fastgltf::AccessorType::Vec3) { + fastgltf::iterateAccessorWithIndex(asset, accessor, [&](const auto& val, std::size_t idx) { + verts[prevVertSize + idx].color = { ToByte(val.x()), ToByte(val.y()), ToByte(val.z()), 255 }; + }); + } else if (accessor.type == fastgltf::AccessorType::Vec4) { + fastgltf::iterateAccessorWithIndex(asset, accessor, [&](const auto& val, std::size_t idx) { + verts[prevVertSize + idx].color = { ToByte(val.x()), ToByte(val.y()), ToByte(val.z()), ToByte(val.w()) }; + }); + } + } break; case hashString("JOINTS_0"): { assert(skinPtr); fastgltf::iterateAccessorWithIndex(asset, accessor, [&](const auto& val, std::size_t idx) { diff --git a/rts/Rendering/Models/VertexData.cpp b/rts/Rendering/Models/VertexData.cpp index 6216040fdce..3eab9be7a8e 100644 --- a/rts/Rendering/Models/VertexData.cpp +++ b/rts/Rendering/Models/VertexData.cpp @@ -1,6 +1,6 @@ #include "VertexData.hpp" -static_assert(sizeof(SVertexData) == (3 + 3 + 3 + 3 + 4 + 2 + 1) * 4); +static_assert(sizeof(SVertexData) == (3 + 3 + 3 + 3 + 4 + 2 + 1 + 1) * 4); void SVertexData::TransformBy(const Transform& transform) { diff --git a/rts/Rendering/Models/VertexData.hpp b/rts/Rendering/Models/VertexData.hpp index 434f17f47c9..abb9907c37e 100644 --- a/rts/Rendering/Models/VertexData.hpp +++ b/rts/Rendering/Models/VertexData.hpp @@ -23,6 +23,7 @@ struct SVertexData { boneIDsLow = DEFAULT_BONEIDS_LOW; boneWeights = DEFAULT_BONEWEIGHTS; boneIDsHigh = DEFAULT_BONEIDS_HIGH; + color = DEFAULT_COLOR; } SVertexData( const float3& p, @@ -42,12 +43,14 @@ struct SVertexData { boneIDsLow = DEFAULT_BONEIDS_LOW; boneWeights = DEFAULT_BONEWEIGHTS; boneIDsHigh = DEFAULT_BONEIDS_HIGH; + color = DEFAULT_COLOR; } static constexpr size_t NUM_MODEL_UVCHANNS = 2; static constexpr std::array DEFAULT_BONEIDS_HIGH = { 255, 255, 255, 255 }; static constexpr std::array DEFAULT_BONEIDS_LOW = { 255, 255, 255, 255 }; static constexpr std::array DEFAULT_BONEWEIGHTS = { 255, 0 , 0, 0 }; + static constexpr std::array DEFAULT_COLOR = { 255, 255, 255, 255 }; static constexpr uint16_t INVALID_BONEID = 0xFFFF; static constexpr size_t MAX_BONES_PER_VERTEX = 4; @@ -61,6 +64,8 @@ struct SVertexData { std::array boneWeights; std::array boneIDsHigh; + std::array color; + template void SetBones(const C& bi) { static_assert(std::is_same_v>);