Fix sample hydration rendering regressions
This commit is contained in:
parent
06010c5922
commit
d52cc2e3b7
@ -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",
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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::<settings::ProjectSettings>()
|
||||
.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::<Visibility>(actor),
|
||||
Some(&Visibility::Hidden),
|
||||
"scene actors must receive authored visibility before Update hydration"
|
||||
);
|
||||
assert!(app.world().get::<InheritedVisibility>(actor).is_some());
|
||||
assert!(app.world().get::<ViewVisibility>(actor).is_some());
|
||||
assert!(
|
||||
app.world().get::<Children>(actor).is_none(),
|
||||
"scene finalization must not create collider children ahead of Update"
|
||||
@ -2511,6 +2521,7 @@ mod tests {
|
||||
.world()
|
||||
.get::<avian3d::prelude::ColliderConstructor>(first_child)
|
||||
.is_some());
|
||||
assert!(app.world().get::<InheritedVisibility>(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::<StandardMaterial>::default())
|
||||
.init_asset::<Mesh>()
|
||||
.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<Entity> = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, (With<LevelObject>, With<BrushDesc>)>()
|
||||
.iter(app.world())
|
||||
.collect();
|
||||
assert!(!brush_actors.is_empty());
|
||||
for actor in &brush_actors {
|
||||
assert!(app.world().get::<InheritedVisibility>(*actor).is_some());
|
||||
assert!(app.world().get::<ViewVisibility>(*actor).is_some());
|
||||
assert!(app.world().get::<Children>(*actor).is_none());
|
||||
}
|
||||
|
||||
app.update();
|
||||
|
||||
for actor in brush_actors {
|
||||
let generated_meshes = app
|
||||
.world()
|
||||
.get::<Children>(actor)
|
||||
.expect("brush hydration should attach generated mesh children");
|
||||
assert!(generated_meshes
|
||||
.iter()
|
||||
.any(|child| app.world().get::<Mesh3d>(child).is_some()));
|
||||
assert!(app.world().get::<InheritedVisibility>(actor).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_serialization_persists_composition_without_composed_members() {
|
||||
let mut app = App::new();
|
||||
|
||||
@ -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<LevelObject>>()
|
||||
.iter(world)
|
||||
.map(|(entity, vis)| (entity, *vis))
|
||||
.collect();
|
||||
|
||||
let rendering = world
|
||||
.get_resource::<settings::ProjectSettings>()
|
||||
.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::<Visibility>(entity) {
|
||||
*vis = visibility;
|
||||
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut.insert(visibility);
|
||||
}
|
||||
if world.get::<InheritedVisibility>(entity).is_none() {
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut.insert(InheritedVisibility::default());
|
||||
}
|
||||
}
|
||||
if world.get::<ViewVisibility>(entity).is_none() {
|
||||
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
|
||||
entity_mut.insert(ViewVisibility::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -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<LevelObject>>()
|
||||
.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::<InheritedVisibility>(entity).is_some());
|
||||
assert!(world.get::<ViewVisibility>(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::<Visibility>(entity), Some(&Visibility::Hidden));
|
||||
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
||||
assert!(world.get::<ViewVisibility>(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::<EditorVisibility>(entity),
|
||||
Some(&EditorVisibility::default())
|
||||
);
|
||||
assert_eq!(world.get::<Visibility>(entity), Some(&Visibility::Visible));
|
||||
assert!(world.get::<InheritedVisibility>(entity).is_some());
|
||||
assert!(world.get::<ViewVisibility>(entity).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user