godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.07k stars 69 forks source link

Improved lightmap baking performance and workflow #7900

Open reduz opened 9 months ago

reduz commented 9 months ago

Describe the project you are working on

Godot

Describe the problem or limitation you are having in your project

Despite the process of baking lightmaps itself in Godot 4 being really fast thanks to the GPU based lightmapper, the rest of the experience is cumbersome and slow on the usability side.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

This is a combination of proposals, both technical and usability related to make the baking process much more comfortable.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Utilize a BSP cache

After finishing lighting, Godot needs to generate a BSP tree from the probes so the probe interpolation process can be efficient. This consisting of:

This is slow and its done every frame. The code is here: https://github.com/godotengine/godot/blob/master/scene/3d/lightmap_gi.cpp#L1129C2-L1129C2 And ends with a call to gi_data->set_capture_data(bounds, interior, points, sh, tetrahedrons, bsp_array, exposure_normalization);

My suggestion here is to do as follows:

Pseudocode:

In this part of the code: https://github.com/godotengine/godot/blob/master/scene/3d/lightmap_gi.cpp#L1129C2-L1129C2 Do:


// { remove this
//  // create tetrahedrons

/ /replace by

uint32_t hash = HASH_MURMUR3_SEED;
for (int i = 0; i < lightmapper->get_bake_probe_count(); i++) {
        Vector3 point = lightmapper->get_bake_probe_point(i);
        hash = hash_murmur3_one_double( point.x, hash );
        hash = hash_murmur3_one_double( point.y, hash );
        hash = hash_murmur3_one_double( point.z, hash );
}

if (hash != gi_data->get_probe_hash()) {
        // create tetrahedrons

//.. skip to end //
     // Call this by adding the hash
     gi_data->set_capture_data(bounds, interior, points, sh, tetrahedrons, bsp_array, exposure_normalization, hash);
}

Keep in mind the hash now needs to be serialized in LightmapGIData. you can do by modifiying these functions like this:


// in LightmapGIData header:

uint32_t probe_hash = 0;

// in .cpp

void LightmapGIData::_set_probe_data(const Dictionary &p_data) {
    ERR_FAIL_COND(!p_data.has("bounds"));
    ERR_FAIL_COND(!p_data.has("points"));
    ERR_FAIL_COND(!p_data.has("tetrahedra"));
    ERR_FAIL_COND(!p_data.has("bsp"));
    ERR_FAIL_COND(!p_data.has("sh"));
    ERR_FAIL_COND(!p_data.has("interior"));
    ERR_FAIL_COND(!p_data.has("baked_exposure"));
        uint32_t phash = 0;
        if (p_data.has("probe_hash")) { // older versions will not have it.
             phash=p_data["probe_hash"];
        }
    set_capture_data(p_data["bounds"], p_data["interior"], p_data["points"], p_data["sh"], p_data["tetrahedra"], p_data["bsp"], p_data["baked_exposure"],phash);
}

Dictionary LightmapGIData::_get_probe_data() const {
    Dictionary d;
    d["bounds"] = get_capture_bounds();
    d["points"] = get_capture_points();
    d["tetrahedra"] = get_capture_tetrahedra();
    d["bsp"] = get_capture_bsp_tree();
    d["sh"] = get_capture_sh();
    d["interior"] = is_interior();
    d["baked_exposure"] = get_baked_exposure();
    d["probe_hash"] = probe_hash;
    return d;
}

Split the UI in Bake and Save

Currently the UI looks like this: image

Ideally, it should look like this:

image

The idea is that there are 3 selectable modes:

How are each implemented?

Separating Bake and Save

Baking should happen as now, but the actual references to textures resources inside LightmapGIData should not be overriden until the save step happens. After baking, they should be overriden in RenderingServer but not the resource. Likewise the LightmapGIData should not be saved untl Save is pressed.

Pressing the revert button will restore in RenderingServer the textures inside LightmapGIData.

The code now is hacky AF:

https://github.com/godotengine/godot/blob/master/scene/3d/lightmap_gi.cpp#L123C1-L123C3

This function should be moved to something like:

void LightmapGI::save_textures(const String& p_path) {
    Vector<Ref<Image>> images;
    for (int i = 0; i < light_texture->get_layers(); i++) {
        images.push_back(light_texture->get_layer_data(i));
    }

    int slice_count = images.size();
    int slice_width = images[0]->get_width();
    int slice_height = images[0]->get_height();

    int slices_per_texture = Image::MAX_HEIGHT / slice_height;
    int texture_count = Math::ceil(slice_count / (float)slices_per_texture);

    ret.resize(texture_count);

    String base_name = p_path;

    int last_count = slice_count % slices_per_texture;
    for (int i = 0; i < texture_count; i++) {
        int texture_slice_count = (i == texture_count - 1 && last_count != 0) ? last_count : slices_per_texture;

        Ref<Image> texture_image = Image::create_empty(slice_width, slice_height * texture_slice_count, false, images[0]->get_format());

        for (int j = 0; j < texture_slice_count; j++) {
            texture_image->blit_rect(images[i * slices_per_texture + j], Rect2i(0, 0, slice_width, slice_height), Point2i(0, slice_height * j));
        }

        String texture_path = texture_count > 1 ? base_name + "_" + itos(i) + ".exr" : base_name + ".exr";

        Ref<ConfigFile> config;
        config.instantiate();

        if (FileAccess::exists(texture_path + ".import")) {
            config->load(texture_path + ".import");
        }

        config->set_value("remap", "importer", "2d_array_texture");
        config->set_value("remap", "type", "CompressedTexture2DArray");
        if (!config->has_section_key("params", "compress/mode")) {
            // User may want another compression, so leave it be, but default to VRAM uncompressed.
            config->set_value("params", "compress/mode", 3);
        }
        config->set_value("params", "compress/channel_pack", 1);
        config->set_value("params", "mipmaps/generate", false);
        config->set_value("params", "slices/horizontal", 1);
        config->set_value("params", "slices/vertical", texture_slice_count);

        config->save(texture_path + ".import");

        Error err = texture_image->save_exr(texture_path, false);
        ERR_FAIL_COND_V(err, ret);
        ResourceLoader::import(texture_path);
        Ref<TextureLayered> t = ResourceLoader::load(texture_path); //if already loaded, it will be updated on refocus?
        ERR_FAIL_COND_V(t.is_null(), ret);
        ret[i] = t;
    }

}

Its important that currently, baking happens WITH NO VRAM COMPRESSION, this needs to be corrected in the new workflow, so we always use VRAM Compression (BC6H on PC, ASTC on Mobile).

When saving, saving textures and saving LightmapGIData should happen at the same time.

If this enhancement will not be used often, can it be worked around with a few lines of script?

N/A

Is there a reason why this should be core and not an add-on in the asset library?

N/A

jcostello commented 9 months ago

In terms of probes, also would it be helpful to have a way to distribute them in the scene. The subdivision works great in small scenes but doesn't do much in large scene. Also you don't need proves everywhere. A node where you can define an area and a separation of probes will be ideal for this.

Also, having the lightmap not saved could help to rebake probes only if they where move or distributed diferently. Is this posible?

mrezai commented 9 months ago

There are a lot of tips in this GDC talk about improvement the bake time, runtime and UX/workflow of lightmapper that could be useful for Godot too: Efficient and impactful lighting with Adaptive Probe Volumes | Unity at GDC 2023 https://www.youtube.com/watch?v=iU7X5xICkc8

Calinou commented 9 months ago

In terms of probes, also would it be helpful to have a way to distribute them in the scene. The subdivision works great in small scenes but doesn't do much in large scene. Also you don't need proves everywhere. A node where you can define an area and a separation of probes will be ideal for this.

Also, having the lightmap not saved could help to rebake probes only if they where move or distributed diferently. Is this posible?

For many types of games, you only need lightprobes to be present near the ground, so I suppose there could be a property to control how far above solid meshes lightprobes should be present.

jcostello commented 9 months ago

But not all solid meshes need it. Imagine for example the TPS demo. You only need probes where the player can go

Calinou commented 9 months ago

Please continue the discussion about lightmap probe placement in https://github.com/godotengine/godot-proposals/issues/7937 https://github.com/godotengine/godot-proposals/issues/7938.

jcostello commented 9 months ago

Please continue the discussion about lightmap probe placement in #7937.

I think is the wrong issue number. its #7938

jcostello commented 9 months ago

@reduz does it make sense to separate the lightmap in diferente texture files for large scenes to make it more stable when baking?