material-texture-colorspace-preview#
Code |
VM.TEX.003 |
|---|---|
Validator |
|
Compatibility |
open-usd |
Tags |
✅ |
Summary#
UsdPreviewSurface materials and textures must use correct color space conventions
Description#
A texture holds either a color or data, and the two need opposite treatment: a base color is sRGB-encoded and must be decoded, a roughness or normal map holds linear values that must be read as authored.
A UsdUVTexture reads its image through inputs:file, which is the same attribute for both.
The expectation follows from the surface shader input the texture is connected to, through any
number of intermediate nodes: a normal-map node, a multiply, a node graph interface.
Surface shader input |
Expected |
|---|---|
|
|
every other input, including roughness, metallic, occlusion, opacity, normal and displacement |
|
The color space is inputs:sourceColorSpace, a typed shader input on UsdUVTexture with
exactly three allowed values, raw, sRGB and auto, which OpenUSD’s own reader consults. It
is not colorSpace metadata and it does not take Gf.ColorSpaceNames tokens, so
srgb_rec709_scene is not a legal value here. Where both inputs:sourceColorSpace and
colorSpace metadata are authored on the same UsdUVTexture, the typed attribute is read, and
sRGB authored as metadata is accepted as an alias.
A texture connected only to a displacement output, or unconnected in a library graph, is out of scope.
Whether the image file’s encoding matches the declared color space cannot be determined from
the scene description. An sRGB-encoded PNG declaring raw is decoded incorrectly, and the USD
holds no evidence of it.
The other three appearance formats declare color space elsewhere, in a different vocabulary,
with a different default. MDL is
VM.TEX.002
and OpenPBR is
VM.TEX.004;
the requirements overview states all four side by side.
Warning
A texture reaching a data input MUST declare raw when its file has three or four
channels.
sourceColorSpace defaults to auto, which OpenUSD defines as: consult the image file’s own
gamma or color-space metadata; failing that, treat the texture as sRGB if it is 8-bit with
three or four channels, and otherwise not. So an 8-bit RGB or RGBA file is decoded; a 16-bit
PNG, an EXR, or a single-channel 8-bit mask is not.
For a data texture that heuristic is wrong whenever it fires. A roughness map painted in an image editor and saved as an 8-bit RGB PNG — the ordinary way such a map is produced — is decoded as though its values were color, and every value is read low:
Value in the file |
Read as, under |
Error |
|---|---|---|
|
|
−80% |
|
|
−57% |
|
|
−15% |
The error differs at each end of the range, so the contrast between rough and smooth areas is stretched as well as shifted. On a normal map each component is changed by a different amount, so the vectors no longer point where they were authored and are no longer unit length, which shows up as lighting that looks like bad geometry.
auto is accepted on a data input when the file is single channel, where it already resolves
to raw. On a file with three or four channels it decodes, so raw has to be declared.
auto remains acceptable on a color input, where the heuristic agrees with convention: an
8-bit PNG or JPG base color is sRGB-encoded and is decoded, while a 16-bit or float color
texture is usually linear already and is left alone. Declaring sRGB still resolves more
predictably, since auto resolves per file while sRGB and raw resolve the same way
everywhere. auto also has no counterpart on OpenPBR, where an undeclared color space means no
transform whatever the file is, so an asset with both surfaces declares a color space
explicitly or the same texture is decoded on one and not the other.
Why is it required?#
A base color texture read as linear renders washed out; a normal map read as sRGB bends its directions and the lighting is wrong in a way that looks like bad geometry
The defect is invisible in the USD text: both cases are a well-formed material with a plausible texture path
A three- or four-channel data texture left to
autois the default output of common image editors, so it is the ordinary caseKeeps color handling consistent between the formats a SimReady asset may include, so the same asset does not render differently depending on which surface a runtime picks
Examples#
Valid: a color texture and a data texture each declaring what its signal needs#
def Material "mtl_bracket"
{
token outputs:surface.connect = </mtl_bracket/Preview.outputs:surface>
def Shader "BaseColorTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_basecolor.png@
token inputs:sourceColorSpace = "sRGB"
float3 outputs:rgb
}
def Shader "RoughnessTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_roughness.png@
token inputs:sourceColorSpace = "raw"
float outputs:r
}
def Shader "Preview"
{
uniform token info:id = "UsdPreviewSurface"
color3f inputs:diffuseColor.connect = </mtl_bracket/BaseColorTex.outputs:rgb>
float inputs:roughness.connect = </mtl_bracket/RoughnessTex.outputs:r>
token outputs:surface
}
}
Invalid: a texture reaching normal and declaring sRGB#
def Material "mtl_bracket"
{
token outputs:surface.connect = </mtl_bracket/Preview.outputs:surface>
def Shader "NormalTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_normal.png@
token inputs:sourceColorSpace = "sRGB"
float3 outputs:rgb
}
def Shader "Preview"
{
uniform token info:id = "UsdPreviewSurface"
normal3f inputs:normal.connect = </mtl_bracket/NormalTex.outputs:rgb>
token outputs:surface
}
}
Invalid: a texture reaching diffuseColor and declaring raw#
def Material "mtl_bracket"
{
token outputs:surface.connect = </mtl_bracket/Preview.outputs:surface>
def Shader "BaseColorTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_basecolor.png@
token inputs:sourceColorSpace = "raw"
float3 outputs:rgb
}
def Shader "Preview"
{
uniform token info:id = "UsdPreviewSurface"
color3f inputs:diffuseColor.connect = </mtl_bracket/BaseColorTex.outputs:rgb>
token outputs:surface
}
}
Invalid: a three-channel roughness map left to auto#
An 8-bit RGB PNG saved out of an image editor. auto decodes it as sRGB, and the roughness
values are read low across the range. The same map saved as a single-channel file would be
left alone by auto and would pass.
def Material "mtl_bracket"
{
token outputs:surface.connect = </mtl_bracket/Preview.outputs:surface>
def Shader "RoughnessTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_roughness.png@
token inputs:sourceColorSpace = "auto"
float outputs:r
}
def Shader "Preview"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:roughness.connect = </mtl_bracket/RoughnessTex.outputs:r>
token outputs:surface
}
}
Invalid: a token outside the UsdUVTexture vocabulary#
linear is not one of the three values inputs:sourceColorSpace takes, so it cannot be
interpreted. A linear data texture declares raw.
def Material "mtl_bracket"
{
token outputs:surface.connect = </mtl_bracket/Preview.outputs:surface>
def Shader "RoughnessTex"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/bracket_roughness.png@
token inputs:sourceColorSpace = "linear"
float outputs:r
}
def Shader "Preview"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:roughness.connect = </mtl_bracket/RoughnessTex.outputs:r>
token outputs:surface
}
}
How to comply#
Decide the signal from the surface input the texture drives, not from the texture’s file name or from the image node’s own inputs.
Author
inputs:sourceColorSpace = "sRGB"on every texture reachingdiffuseColor,emissiveColororspecularColor.Author
inputs:sourceColorSpace = "raw"on every other texture, unless the file is single channel, whereautoalready resolves toraw. On a three- or four-channel fileautodecodes, and every value is read low.Use the
UsdUVTexturevocabulary.raw,sRGBandautoare the only legal values;Gf.ColorSpaceNamestokens such assrgb_rec709_scenebelong tocolorSpacemetadata and are not read here.Save the texture files in the encoding the value claims. An sRGB-encoded file declaring
raw, or a linear one declaringsRGB, is decoded wrongly, and the USD holds no evidence of it.
Accepted values#
Authored |
On a color input |
On a data input |
|---|---|---|
|
author this |
reported |
|
reported |
author this |
|
accepted |
reported, unless the file is single channel |
absent |
accepted — the attribute defaults to |
reported, unless the file is single channel |
any other token |
reported |
reported |
auto is accepted on a color input, where the heuristic agrees with convention. On a data
input it is accepted only for a single-channel file, which the heuristic leaves alone, and
reported for a file with three or four channels, which it decodes.