Capture Bounds with an orthographic camera -- runtime world map

I have bounds for a group of objects (calculated with Bounds.Encapsulate on the group). How can I capture an image of the objects to a texture?

For example, the objects are my level and I want to take an overhead capture of them to use as an in-game map. I want to precisely capture the bounds to make it easy to map a position within the bounds to a position within the map.

Setup an orthographic camera where the size is set to the z of the bounds and render to a render texture with the same aspect ratio as the bounds.

Here’s my solution:

public RawImage _MapImage;
// AspectRatioFitter ensures the resulting image matches the
// bounds dimensions and allows converting between the
// normalized position within the bounds to the position
// within the map (for drawing positions on the map).
public AspectRatioFitter _AspectFitter;
public Bounds _WorldBounds;

void CaptureMapImage() {
    var aspect_ratio = _WorldBounds.extents.x / _WorldBounds.extents.z;
    var t = _MapImage.transform as RectTransform;
    var height = Mathf.RoundToInt(t.rect.size.y);
    var width  = Mathf.RoundToInt(height * aspect_ratio);
    _AspectFitter.aspectRatio = aspect_ratio;

    // To make the map a live view of the world, allocate and
    // store RenderTexture instead of temporary and don't
    // destroy the camera.
    var render_target = RenderTexture.GetTemporary(width, height, 16);

    var cam = new GameObject("overhead map camera").AddComponent<Camera>();
    cam.targetTexture = render_target;
    cam.renderingPath = RenderingPath.Forward;

    // Clear solid color so empty areas are transparent
    cam.clearFlags = CameraClearFlags.SolidColor;
    cam.backgroundColor = Color.clear;

    // You may need to play with offset. I wanted it close to
    // my main ground plane so ceilings are not visible. You
    // may want it at _WorldBounds.extent.y height.
    cam.orthographic = true;
    var offset = Vector3.up;
    cam.transform.SetPositionAndRotation(_WorldBounds.center + offset, Quaternion.LookRotation(Vector3.down));
    cam.orthographicSize = _WorldBounds.extents.z;

    cam.Render();
    Util.DestroyGameObject(cam.gameObject);

    var tex = new Texture2D(render_target.width, render_target.height);
    var rect = new Rect(0, 0, tex.width, tex.height);

    var old_rt = RenderTexture.active;
    RenderTexture.active = render_target;
    tex.ReadPixels(rect, 0, 0);
    tex.Apply();
    RenderTexture.active = old_rt;

    RenderTexture.ReleaseTemporary(render_target);

    _MapImage.texture = tex;
}

// Use this to position world objects on the map.
void PlaceOnMap(Transform world_obj, RectTransform map_obj) {
    var pos = OverheadToMap(world_obj.position);
    map_obj.anchoredPosition = pos;

    var yaw = world_obj.rotation.eulerAngles.y;
    map_obj.rotation = Quaternion.Euler(0f, 0f, -yaw);
}

// 
// Helper math functions.
//

Vector2 OverheadToMap(Vector3 v) {
    var pos = OverheadTo2D(v - _WorldBounds.center);
    var world_size = OverheadTo2D(_WorldBounds);
    var pos_normalized = Vector2.Scale(pos, Inverse(world_size));
    var t = _MapImage.transform as RectTransform;
    var map_size = t.rect.size;
    return Vector2.Scale(pos_normalized, map_size);
}

Vector2 OverheadTo2D(Vector3 v) {
    return new Vector2(v.x, v.z);
}

Vector2 Inverse(Vector2 v) {
    return new Vector2(1f / v.x, 1f / v.y);
}