Every colour-valued public API in the engine is sRGB/gamma-encoded and converted to linear at upload — except SpriteComponent#color, which is passed through raw.
The inconsistency
SpriteComponent writes the tint straight into the material_emissive uniform with no conversion:
// src/framework/components/sprite/component.js:419-422 (and again at 903)
this._colorUniform[0] = this._color.r;
this._colorUniform[1] = this._color.g;
this._colorUniform[2] = this._color.b;
this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform);
ElementComponent writes the same uniform but converts first, and says so:
// src/framework/components/element/image-element.js:_updateRenderableEmissive
// color uniforms are in linear space
_tempColor.linear(this._color);
this._renderable.setParameter('material_emissive', this._colorUniform);
Same pattern in text-element.js for color, outlineColor and shadowColor.
Sprites use a StandardMaterial (src/framework/components/sprite/system.js), and StandardMaterial's own emissive also goes through the conversion — _defineColor in src/scene/materials/standard-material.js does _tempColor.linear(color) with the comment "uniforms are always in linear space". So the same uniform, on the same material class, has two different conventions depending on which component sets it.
Effect
sprite.color = new Color(0.5, 0.5, 0.5) reaches the shader as linear 0.5, whereas the same tint on an ElementComponent image — or material.emissive = (0.5, 0.5, 0.5) — reaches it as ~0.218. That is roughly 2.3x brighter in linear terms at mid-grey.
The two conventions agree exactly at 0 and 1, which is presumably why this has gone unnoticed: only mid-tone tints diverge.
Notes
- Found by source inspection while auditing colour spaces across the engine; not yet confirmed by rendering a sprite side by side with an equivalent Element image, so worth a visual check first.
- Fixing it changes the appearance of existing mid-tone sprite tints, so it is effectively a breaking visual change and probably wants to land on a major.
- The alternative (documenting
SpriteComponent#color as linear) leaves the engine internally inconsistent, and inconsistent with ElementComponent, which seems worse.
Every colour-valued public API in the engine is sRGB/gamma-encoded and converted to linear at upload — except
SpriteComponent#color, which is passed through raw.The inconsistency
SpriteComponentwrites the tint straight into thematerial_emissiveuniform with no conversion:ElementComponentwrites the same uniform but converts first, and says so:Same pattern in
text-element.jsforcolor,outlineColorandshadowColor.Sprites use a
StandardMaterial(src/framework/components/sprite/system.js), andStandardMaterial's ownemissivealso goes through the conversion —_defineColorinsrc/scene/materials/standard-material.jsdoes_tempColor.linear(color)with the comment "uniforms are always in linear space". So the same uniform, on the same material class, has two different conventions depending on which component sets it.Effect
sprite.color = new Color(0.5, 0.5, 0.5)reaches the shader as linear 0.5, whereas the same tint on anElementComponentimage — ormaterial.emissive = (0.5, 0.5, 0.5)— reaches it as ~0.218. That is roughly 2.3x brighter in linear terms at mid-grey.The two conventions agree exactly at 0 and 1, which is presumably why this has gone unnoticed: only mid-tone tints diverge.
Notes
SpriteComponent#coloras linear) leaves the engine internally inconsistent, and inconsistent withElementComponent, which seems worse.