fromKtx2Bytes static method

Future<EnvironmentMap> fromKtx2Bytes(
  1. Uint8List bytes, {
  2. List<Vector3>? diffuseSphericalHarmonics,
  3. Uint8List? diffuseShSidecar,
})

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(),
  );
}