diff --git a/assets/.index/registry.ron b/assets/.index/registry.ron index e36143f..7b190f8 100644 --- a/assets/.index/registry.ron +++ b/assets/.index/registry.ron @@ -287,6 +287,42 @@ ), dependencies: [], ), + ( + id: ("607864bf-fa77-4f9e-acc9-2c0906d34163"), + path: "assets/levels/samples/material_lab.scn.ron", + label: "material_lab.scn", + kind_tag: "Level", + import_settings: ( + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SingleActor, + material_policy: SourceMaterials, + static_mesh_manifest_path: None, + animation_manifest_path: None, + default_animation_clip_id: None, + ), + dependencies: [], + ), + ( + id: ("bca13177-afa9-4691-9b40-288059c7a57e"), + path: "assets/levels/samples/brush_blockout.scn.ron", + label: "brush_blockout.scn", + kind_tag: "Level", + import_settings: ( + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SingleActor, + material_policy: SourceMaterials, + static_mesh_manifest_path: None, + animation_manifest_path: None, + default_animation_clip_id: None, + ), + dependencies: [], + ), ( id: ("5a2307d0-48e5-40d3-a5c9-527c0cfd30f4"), path: "assets/levels/editor_scene.scn.ron", @@ -395,6 +431,24 @@ ), dependencies: [], ), + ( + id: ("e42185de-cda2-4a9e-9df0-df67ccd86123"), + path: "assets/samples/editor_samples.ron", + label: "editor_samples", + kind_tag: "Level", + import_settings: ( + scale: 1.0, + generate_collider: true, + lod0_only: true, + placement_mode: StaticAsset, + hierarchy_mode: SingleActor, + material_policy: SourceMaterials, + static_mesh_manifest_path: None, + animation_manifest_path: None, + default_animation_clip_id: None, + ), + dependencies: [], + ), ( id: ("04a4e00e-4732-43db-8e99-9cd99b94667b"), path: "assets/animations/generated/113f74df-e39c-41d4-9b5b-e48efe541f7f.animation.ron", diff --git a/crates/blacksite_surface/src/lib.rs b/crates/blacksite_surface/src/lib.rs index 727a154..43b9276 100644 --- a/crates/blacksite_surface/src/lib.rs +++ b/crates/blacksite_surface/src/lib.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::fs; +use bevy::material::OpaqueRendererMethod; use bevy::mesh::MeshVertexBufferLayoutRef; use bevy::pbr::{ ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline, @@ -393,6 +394,9 @@ fn build_terrain_layer_material( base: StandardMaterial { base_color: Color::WHITE, perceptual_roughness: 1.0, + // Terrain is intentionally absent from the Solari acceleration structure until its + // ray-tracing evaluator exists, so keep the layer blend in the raster forward pass. + opaque_render_method: OpaqueRendererMethod::Forward, ..default() }, extension, @@ -1079,6 +1083,10 @@ mod tests { assert_eq!(material.extension.uniform.uv_scales.y, 10.0); assert_eq!(material.extension.uniform.properties[0].w, 1.0); assert_eq!(material.extension.uniform.properties[1].w, 1.0); + assert_eq!( + material.base.opaque_render_method, + OpaqueRendererMethod::Forward + ); assert_ne!( material.extension.uniform.base_colors[0], material.extension.uniform.base_colors[1] diff --git a/crates/editor/src/scene/scene_io.rs b/crates/editor/src/scene/scene_io.rs index a52c586..b3fb3df 100644 --- a/crates/editor/src/scene/scene_io.rs +++ b/crates/editor/src/scene/scene_io.rs @@ -2171,6 +2171,8 @@ fn load_composed_subscenes( } fn finalize_scene_load(world: &mut World) { + shared::initialize_level_object_visibility_hierarchy(world); + let rendering = world .resource::() .rendering @@ -2490,12 +2492,20 @@ mod tests { .world_mut() .spawn(( LevelObject, + EditorVisibility { visible: false }, BrushDesc::default(), ColliderDesc::static_cuboid(Vec3::ONE), )) .id(); finalize_scene_load(app.world_mut()); + assert_eq!( + app.world().get::(actor), + Some(&Visibility::Hidden), + "scene actors must receive authored visibility before Update hydration" + ); + assert!(app.world().get::(actor).is_some()); + assert!(app.world().get::(actor).is_some()); assert!( app.world().get::(actor).is_none(), "scene finalization must not create collider children ahead of Update" @@ -2511,6 +2521,7 @@ mod tests { .world() .get::(first_child) .is_some()); + assert!(app.world().get::(actor).is_some()); app.update(); assert_eq!( @@ -2522,6 +2533,57 @@ mod tests { ); } + #[test] + fn brush_sample_parents_own_visibility_before_generated_meshes_attach() { + let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .unwrap(); + let sample_path = project_root.join("assets/levels/samples/brush_blockout.scn.ron"); + let mut app = App::new(); + app.add_plugins(MinimalPlugins) + .add_plugins(AssetPlugin::default()) + .add_plugins(bevy::pbr::MaterialPlugin::::default()) + .init_asset::() + .add_plugins(shared::SharedTypesPlugin); + app.world_mut() + .insert_resource(settings::ProjectSettings::default()); + app.world_mut() + .insert_resource(crate::project_io::ProjectWorkspace { + root: project_root.display().to_string(), + ..Default::default() + }); + app.world_mut() + .insert_resource(crate::ui::hierarchy_state::HierarchyPanelState::default()); + + load_level(app.world_mut(), &sample_path).unwrap(); + + let brush_actors: Vec = app + .world_mut() + .query_filtered::, With)>() + .iter(app.world()) + .collect(); + assert!(!brush_actors.is_empty()); + for actor in &brush_actors { + assert!(app.world().get::(*actor).is_some()); + assert!(app.world().get::(*actor).is_some()); + assert!(app.world().get::(*actor).is_none()); + } + + app.update(); + + for actor in brush_actors { + let generated_meshes = app + .world() + .get::(actor) + .expect("brush hydration should attach generated mesh children"); + assert!(generated_meshes + .iter() + .any(|child| app.world().get::(child).is_some())); + assert!(app.world().get::(actor).is_some()); + } + } + #[test] fn scene_serialization_persists_composition_without_composed_members() { let mut app = App::new(); diff --git a/crates/shared/src/hydration/mod.rs b/crates/shared/src/hydration/mod.rs index b6a2d9f..c42fe5a 100644 --- a/crates/shared/src/hydration/mod.rs +++ b/crates/shared/src/hydration/mod.rs @@ -40,16 +40,17 @@ use static_meshes::{ }; pub use terrain::HydratedTerrainChunk; use terrain::{cleanup_removed_terrain, hydrate_terrain}; +pub use visibility::initialize_level_object_visibility_hierarchy; use visibility::{ ensure_level_object_visibility_hierarchy, init_editor_visibility_on_spawn, - sync_editor_visibility, visibility_from_editor, + sync_editor_visibility, }; use crate::{ - authoring_component_active, AuthoringComponentStates, BrushDesc, ColliderDesc, - EditorVisibility, InspectorOrder, LevelObject, MaterialDesc, MaterialOverride, Primitive, - StaticMeshRenderer, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC, - COMPONENT_MATERIAL_DESC, COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER, + authoring_component_active, AuthoringComponentStates, BrushDesc, ColliderDesc, InspectorOrder, + LevelObject, MaterialDesc, MaterialOverride, Primitive, StaticMeshRenderer, + COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC, + COMPONENT_PRIMITIVE, COMPONENT_STATIC_MESH_RENDERER, }; /// Registers hydration systems in deterministic order. @@ -115,6 +116,8 @@ pub fn flush_level_object_hydration(world: &mut World) { world.insert_resource(StaticMeshArtifactCache::default()); } + initialize_level_object_visibility_hierarchy(world); + let primitives: Vec<(Entity, Primitive)> = world .query_filtered::<( Entity, @@ -250,12 +253,6 @@ pub fn flush_level_object_hydration(world: &mut World) { } } - let visibility_targets: Vec<(Entity, EditorVisibility)> = world - .query_filtered::<(Entity, &EditorVisibility), With>() - .iter(world) - .map(|(entity, vis)| (entity, *vis)) - .collect(); - let rendering = world .get_resource::() .map(|s| s.rendering.clone()) @@ -348,25 +345,6 @@ pub fn flush_level_object_hydration(world: &mut World) { } state.apply(world); - - for (entity, editor) in visibility_targets { - let visibility = visibility_from_editor(editor); - if let Some(mut vis) = world.get_mut::(entity) { - *vis = visibility; - } else if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.insert(visibility); - } - if world.get::(entity).is_none() { - if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.insert(InheritedVisibility::default()); - } - } - if world.get::(entity).is_none() { - if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.insert(ViewVisibility::default()); - } - } - } } #[cfg(test)] diff --git a/crates/shared/src/hydration/visibility.rs b/crates/shared/src/hydration/visibility.rs index 77e42c0..5e48f34 100644 --- a/crates/shared/src/hydration/visibility.rs +++ b/crates/shared/src/hydration/visibility.rs @@ -3,6 +3,24 @@ use crate::{EditorVisibility, LevelObject}; use bevy::prelude::*; +/// Establishes the runtime visibility hierarchy synchronously for all authored actors. +/// +/// Scene loading can occur between scheduled hydration sets, so callers that materialize a scene +/// must run this before generated render children can be attached in the same frame. +pub fn initialize_level_object_visibility_hierarchy(world: &mut World) { + let targets: Vec<(Entity, EditorVisibility)> = world + .query_filtered::<(Entity, Option<&EditorVisibility>), With>() + .iter(world) + .map(|(entity, visibility)| (entity, visibility.copied().unwrap_or_default())) + .collect(); + + for (entity, editor_visibility) in targets { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.insert((editor_visibility, visibility_from_editor(editor_visibility))); + } + } +} + #[expect( clippy::type_complexity, reason = "the query mirrors Bevy's three visibility hierarchy components" @@ -81,7 +99,9 @@ pub fn visibility_from_editor(editor: EditorVisibility) -> Visibility { #[cfg(test)] mod tests { - use super::ensure_level_object_visibility_hierarchy; + use super::{ + ensure_level_object_visibility_hierarchy, initialize_level_object_visibility_hierarchy, + }; use crate::{EditorVisibility, LevelObject}; use bevy::prelude::*; @@ -102,4 +122,34 @@ mod tests { assert!(world.get::(entity).is_some()); assert!(world.get::(entity).is_some()); } + + #[test] + fn immediate_initializer_establishes_hierarchy_before_scheduled_hydration() { + let mut world = World::new(); + let entity = world + .spawn((LevelObject, EditorVisibility { visible: false })) + .id(); + + initialize_level_object_visibility_hierarchy(&mut world); + + assert_eq!(world.get::(entity), Some(&Visibility::Hidden)); + assert!(world.get::(entity).is_some()); + assert!(world.get::(entity).is_some()); + } + + #[test] + fn immediate_initializer_migrates_missing_authored_visibility() { + let mut world = World::new(); + let entity = world.spawn(LevelObject).id(); + + initialize_level_object_visibility_hierarchy(&mut world); + + assert_eq!( + world.get::(entity), + Some(&EditorVisibility::default()) + ); + assert_eq!(world.get::(entity), Some(&Visibility::Visible)); + assert!(world.get::(entity).is_some()); + assert!(world.get::(entity).is_some()); + } } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 8fd3b7b..c86f81c 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -20,10 +20,10 @@ pub use actor::{infer_actor_kind, validate_actor, ActorValidationError}; pub use animation::*; pub use components::*; pub use hydration::{ - cascade_config_from_rendering, flush_level_object_hydration, material_from_desc, - strip_hydrated, strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember, - HydratedPrefabReady, HydratedSkinnedMeshRoot, HydratedTerrainChunk, HydrationPlugin, - PrefabHydrationBlocked, + cascade_config_from_rendering, flush_level_object_hydration, + initialize_level_object_visibility_hierarchy, material_from_desc, strip_hydrated, + strip_hydrated_entity, HydratedModelRoot, HydratedPrefabMember, HydratedPrefabReady, + HydratedSkinnedMeshRoot, HydratedTerrainChunk, HydrationPlugin, PrefabHydrationBlocked, }; pub use material_asset::{ load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset, diff --git a/docs/adr/0040-terrain-material-layer-weights.md b/docs/adr/0040-terrain-material-layer-weights.md index 454334b..34cd51b 100644 --- a/docs/adr/0040-terrain-material-layer-weights.md +++ b/docs/adr/0040-terrain-material-layer-weights.md @@ -22,7 +22,8 @@ weights through `Mesh::ATTRIBUTE_COLOR`. `blacksite_surface` owns the terrain-specific `ExtendedMaterial` and blends each resolved layer's albedo, tangent-space normal, metallic, and roughness inputs. Invalid or unavailable references leave the existing visible terrain fallback in place and emit diagnostics. Terrain remains excluded from -Solari geometry until a matching ray-tracing evaluator exists. +Solari geometry until a matching ray-tracing evaluator exists, and its opaque material is therefore +forced through Bevy's forward raster path even when the project default is deferred. Editor paint strokes materialize the implicit default map only when needed. Each pointer stroke is a single reflected `TerrainDesc` transaction; cancel restores the exact original descriptor. diff --git a/docs/editor/terrain.md b/docs/editor/terrain.md index 028c79c..d6764de 100644 --- a/docs/editor/terrain.md +++ b/docs/editor/terrain.md @@ -59,9 +59,10 @@ references deliberately validate terrain dependency resolution and layer transpo fallback rather than missing geometry. - Generated `HydratedTerrainChunk` children own mesh, optional trimesh collider, and shadow state. They are runtime-only and never serialized as authored actors. -- Foundation chunks are explicitly excluded from Solari submission and remain raster-visible while - Auto/Solari is active. Raster material-layer parity is shipped; matching Solari terrain evaluation - remains explicit future work, and the editor never substitutes a semantically different proxy. +- Foundation chunks are explicitly excluded from Solari submission and force their layer blend + through Bevy's forward opaque raster path while Auto/Solari is active. Raster material-layer + parity is shipped; matching Solari terrain evaluation remains explicit future work, and the editor + never substitutes a semantically different proxy. The terrain foundation is recorded in [ADR 0039](../adr/0039-inline-height-grid-terrain-foundation.md), and layer weight persistence/render transport in