Skip to main content

khora_data/ecs/systems/
ibl_bake.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! One-time IBL environment bake, wired as a data-driven system.
16//!
17//! Runs in [`TickPhase::PreExtract`] so the baked environment is ready before
18//! any lit lane renders. The [`IblBaker`] bakes only on its first call and is
19//! a cheap no-op every frame after — no dirty tracking needed here.
20
21use std::sync::Arc;
22
23use khora_core::lane::OutputDeck;
24use khora_core::math::Vec3;
25use khora_core::renderer::api::resource::CpuTexture;
26use khora_core::renderer::light::LightType;
27use khora_core::renderer::traits::PipelineSystem;
28use khora_core::renderer::GraphicsDevice;
29use khora_core::Runtime;
30
31use crate::ecs::{DataSystemRegistration, GlobalTransform, Light, TickPhase, World};
32use crate::{AssetStore, EnvironmentMap, IblBaker};
33
34/// Returns the world-space direction **toward** the scene's first enabled
35/// directional light, or `Vec3::ZERO` when there is none (the bake then falls
36/// back to its own default sun).
37///
38/// The light's travel direction is derived exactly as the render extraction
39/// does it (`rotation * direction`); the sun sits opposite that.
40fn sun_direction(world: &World) -> Vec3 {
41    for (light, transform) in world.query::<(&Light, &GlobalTransform)>() {
42        if !light.enabled {
43            continue;
44        }
45        if let LightType::Directional(dir_light) = &light.light_type {
46            return -(transform.0.rotation() * dir_light.direction);
47        }
48    }
49    Vec3::ZERO
50}
51
52fn ibl_bake_system(world: &mut World, runtime: &Runtime, _deck: &mut OutputDeck) {
53    let Some(baker) = runtime.resources.get::<IblBaker>() else {
54        return;
55    };
56    if baker.is_baked() {
57        return;
58    }
59    let Some(device) = runtime.backends.get::<Arc<dyn GraphicsDevice>>() else {
60        return;
61    };
62    let Some(pipeline_system) = runtime.resources.get::<Arc<dyn PipelineSystem>>() else {
63        return;
64    };
65    let sun = sun_direction(world);
66
67    // An authored equirectangular environment, when the scene selected one and
68    // the asset is loaded. The read guard is held across the bake so the
69    // texture cannot be evicted mid-projection; absent ⇒ procedural sky.
70    let env_store = runtime
71        .resources
72        .get::<EnvironmentMap>()
73        .and_then(|env| env.texture)
74        .and_then(|uuid| {
75            let store = runtime.resources.get::<AssetStore>()?.store::<CpuTexture>();
76            Some((uuid, store))
77        });
78    match env_store {
79        Some((uuid, store)) => {
80            let guard = match store.read() {
81                Ok(guard) => guard,
82                Err(_) => {
83                    log::error!("ibl_bake: CpuTexture store poisoned; baking procedural sky");
84                    baker.ensure_baked(device.as_ref(), pipeline_system.as_ref(), sun, None);
85                    return;
86                }
87            };
88            if guard.get(&uuid).is_none() {
89                // Still loading — retry next tick rather than baking a
90                // procedural sky the scene did not ask for. Bounded: the lit
91                // lanes do not render until the bake publishes its bindings,
92                // so an asset that never arrives must not stall them forever.
93                if baker.wait_for_environment() {
94                    return;
95                }
96                log::warn!(
97                    "ibl_bake: environment texture {uuid:?} did not load in time; \
98                     baking the procedural sky instead"
99                );
100                drop(guard);
101                baker.ensure_baked(device.as_ref(), pipeline_system.as_ref(), sun, None);
102                return;
103            }
104            baker.ensure_baked(
105                device.as_ref(),
106                pipeline_system.as_ref(),
107                sun,
108                guard.get(&uuid).map(|handle| &**handle),
109            );
110        }
111        None => baker.ensure_baked(device.as_ref(), pipeline_system.as_ref(), sun, None),
112    }
113}
114
115inventory::submit! {
116    DataSystemRegistration {
117        name: "ibl_bake",
118        phase: TickPhase::PreExtract,
119        run: ibl_bake_system,
120        order_hint: -10,
121        runs_after: &[],
122    }
123}