fromKtx2Bytes static method
Loads an EnvironmentMap from a pre-baked KTX2 radiance cubemap, the output of an offline image-based-lighting bake (the Khronos glTF IBL sampler, or prefilterEquirectRadianceToCube run in tooling).
The file must be a cubemap (faceCount 6) with square power-of-two faces
at least kMinRadianceCubeSize a side, storing an uncompressed
R16G16B16A16_SFLOAT, R32G32B32A32_SFLOAT, E5B9G9R9_UFLOAT_PACK32,
B10G11R11_UFLOAT_PACK32, or R8G8B8A8_UNORM/_SRGB payload, optionally
zstd-supercompressed. Block-compressed and Basis payloads are rejected;
a GGX radiance chain is high dynamic range and a block codec destroys it.
Anything else throws a FormatException.
Mip-to-roughness convention. The mip chain must be a GGX roughness
series with linear perceptual roughness per level,
roughness = level / (levelCount - 1), mip 0 being the mirror level and
the last level fully rough. That is the engine's own convention
(prefilterEquirectRadianceToCube bakes mip i at
i / (kPrefilterBandCount - 1)), and it is what the shader assumes when
it samples at lod = roughness * (kPrefilterBandCount - 1). The shader's
lod scale is a constant, not the texture's mip count, so a chain of any
other length is resampled here onto exactly kPrefilterBandCount levels
(interpolating the two source levels bracketing each band's roughness).
A file baked with a non-linear roughness distribution shades differently
from an internally prefiltered environment; re-bake it linearly.
Face order. Faces are taken in KTX2 order (+X, -X, +Y, -Y, +Z, -Z)
with no reordering and no flip. The engine's cube sampling is the
Khronos/Vulkan/GL convention verbatim (see cubeFaceBases), so a
conforming file lands correctly as stored. Reorienting an environment for
a scene is Scene.environmentTransform, not the loader's business.
Diffuse. diffuseSphericalHarmonics wins when given; otherwise
diffuseShSidecar is parsed (parseDiffuseShSidecar); otherwise the
file's kDiffuseShKtx2Key key/value entry is read; otherwise the diffuse
term is zero and only reflections light the scene. Coefficients are
irradiance-domain, with the Lambertian A_l band factors and the 1/pi
BRDF term already folded in exactly as
computeDiffuseSphericalHarmonics returns them. Check a bake against the
contract with describeDiffuseSphericalHarmonics.
The parse and resample run on a background isolate; only the upload touches the main thread.
Implementation
static Future<EnvironmentMap> fromKtx2Bytes(
Uint8List bytes, {
List<Vector3>? diffuseSphericalHarmonics,
Uint8List? diffuseShSidecar,
}) async {
final sidecarSh = diffuseShSidecar == null
? null
: parseDiffuseShSidecar(diffuseShSidecar);
// The layout the backend can sample decides which resample runs, so it is
// resolved here and carried into the isolate.
final cubeLayout = effectiveMipRadianceLayout;
final decoded = await compute(_decodeKtx2EnvironmentOnIsolate, (
bytes,
cubeLayout,
));
final (radiance, fileSh) = cubeLayout
? _uploadRadianceCube(decoded as ImportedRadianceCube)
: _uploadRadianceAtlas(decoded as ImportedRadianceAtlas);
return EnvironmentMap._(
radiance,
diffuseSphericalHarmonics ??
sidecarSh ??
fileSh ??
_zeroSphericalHarmonics(),
);
}