bakeImpostor function

Future<ImpostorBake> bakeImpostor({
  1. required ModelProject project,
  2. required ImpostorAtlas atlas,
  3. required Vector3 centre,
  4. required double distance,
  5. required TileDevice tileDevice,
  6. double fovYRadians = math.pi / 8,
  7. RenderSettings settings = const RenderSettings(),
  8. Vector4? clearColor,
})

project rendered from every one of atlas's angles, composited into one atlas image.

Each view is a RenderSnapshotJob over tileDevice — pro-rn-02's own tiled job, at one cell's resolution, from the camera ImpostorAtlas.cameraFor gives. The background is transparent unless clearColor says otherwise: an impostor is a silhouette on a card, and a card baked against a sky carries that sky into every scene it stands in.

One job per view rather than one grid over all of them. The tiles of a snapshot share a camera and these do not, so the eight are eight renders however they are arranged — which is what keeps the composite here rather than inside a job that has no idea it is filling an atlas.

Bake from the distance the impostor will be shown at, through whatever fovYRadians frames the subject there. The perspective is baked into the picture: a card baked from eight metres and shown from three hundred carries eight metres' worth of convergence into a view that has almost none, and the silhouette is visibly the wrong shape rather than merely soft — measured at about fourteen percent of it in flutter3d_cpu's own impostor_card_test.dart, against three at the matching distance. distance and ImpostorCard.framing's own argument are the same number for that reason.

Implementation

Future<ImpostorBake> bakeImpostor({
  required ModelProject project,
  required ImpostorAtlas atlas,
  required Vector3 centre,
  required double distance,
  required TileDevice tileDevice,
  double fovYRadians = math.pi / 8,
  RenderSettings settings = const RenderSettings(),
  Vector4? clearColor,
}) async {
  final int cell = atlas.cellSize;
  final rgba = Uint8List(atlas.size * atlas.size * 4);
  for (var i = 0; i < atlas.angles; i++) {
    final Uint8List png = await RenderSnapshotJob(
      project,
      RenderPreset(
        width: cell,
        height: cell,
        camera: atlas.cameraFor(
          i,
          centre: centre,
          distance: distance,
          fovYRadians: fovYRadians,
        ),
        settings: settings,
        clearColor: clearColor ?? Vector4.zero(),
      ),
      tileDevice: tileDevice,
    ).run();
    // The bytes came straight out of the encoder this repository wrote, so a
    // null here is not a file somebody chose badly — it is the writer and the
    // reader disagreeing, which is a bug rather than a case to handle.
    final DecodedImage? view = decodePng(png);
    if (view == null) {
      throw StateError(
        'the snapshot of impostor view $i did not decode as a PNG',
      );
    }
    final ImpostorView where = atlas.viewAt(i);
    final int x0 = (where.cell.x * atlas.size).round();
    final int y0 = (where.cell.y * atlas.size).round();
    // Row by row: the cell is narrower than the atlas, so the two have
    // different strides and there is no single range to copy.
    for (var y = 0; y < cell; y++) {
      rgba.setRange(
        ((y0 + y) * atlas.size + x0) * 4,
        ((y0 + y) * atlas.size + x0 + cell) * 4,
        view.rgba,
        y * view.width * 4,
      );
    }
  }
  return (atlas: atlas, size: atlas.size, rgba: rgba);
}