Fix material categories and transparent background ordering
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
#![allow(unsafe_code)]
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use fparkan_render::LegacyDepthMode;
|
use fparkan_render::{LegacyDepthMode, LegacyPipelineState};
|
||||||
|
|
||||||
use super::smoke_types::resolve_draw_texture_indices;
|
use super::smoke_types::resolve_draw_texture_indices;
|
||||||
use super::update_material_texture_descriptor;
|
use super::update_material_texture_descriptor;
|
||||||
@@ -434,6 +434,7 @@ impl VulkanSmokeRenderer {
|
|||||||
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
|
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
|
||||||
draw_order_dirty: false,
|
draw_order_dirty: false,
|
||||||
draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()],
|
draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()],
|
||||||
|
world_transparent_background_ranges: Vec::new(),
|
||||||
draw_range_base_pipeline_states: create_info
|
draw_range_base_pipeline_states: create_info
|
||||||
.mesh
|
.mesh
|
||||||
.draw_ranges
|
.draw_ranges
|
||||||
@@ -878,6 +879,74 @@ impl VulkanSmokeRenderer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the active MAT0 phase's blend and alpha-test category to one
|
||||||
|
/// range while retaining its authored depth and cull state.
|
||||||
|
pub fn set_draw_range_material_category(
|
||||||
|
&mut self,
|
||||||
|
range_index: usize,
|
||||||
|
category: u8,
|
||||||
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let base_state = *self
|
||||||
|
.draw_range_base_pipeline_states
|
||||||
|
.get(range_index)
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "dynamic draw range index is out of bounds",
|
||||||
|
})?;
|
||||||
|
let world_queue = self
|
||||||
|
.draw_range_world_queue
|
||||||
|
.get(range_index)
|
||||||
|
.copied()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "dynamic draw range index is out of bounds",
|
||||||
|
})?;
|
||||||
|
// Fixed sky, sprite, weather, and shadow ranges own explicit states.
|
||||||
|
// MAT0 categories apply only after a range has entered the world queue.
|
||||||
|
let (next_base, next_state) =
|
||||||
|
world_material_pipeline_states(base_state, category, world_queue).ok_or(
|
||||||
|
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "MAT0 pipeline category is outside the native 0..=4 table",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let range = self.draw_ranges.get_mut(range_index).ok_or(
|
||||||
|
VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "dynamic draw range index is out of bounds",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
self.draw_range_base_pipeline_states[range_index] = next_base;
|
||||||
|
let pipeline_changed = range.pipeline_state != next_state;
|
||||||
|
range.pipeline_state = next_state;
|
||||||
|
if pipeline_changed {
|
||||||
|
self.rebuild_pipeline_resources()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places nebula, dome, and cloud ranges between opaque and transparent
|
||||||
|
/// world draws. The list is explicit so unrelated fixed passes keep their
|
||||||
|
/// established order.
|
||||||
|
pub fn set_world_transparent_background_ranges(
|
||||||
|
&mut self,
|
||||||
|
range_indices: &[usize],
|
||||||
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let mut seen = vec![false; self.draw_ranges.len()];
|
||||||
|
for &range_index in range_indices {
|
||||||
|
let Some(slot) = seen.get_mut(range_index) else {
|
||||||
|
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "world background range index is out of bounds",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if *slot || self.draw_range_world_queue[range_index].is_some() {
|
||||||
|
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "world background range is duplicated or already queued",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*slot = true;
|
||||||
|
}
|
||||||
|
self.world_transparent_background_ranges = range_indices.to_vec();
|
||||||
|
self.draw_order_dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks a draw range as belonging to the native transparent queue.
|
/// Marks a draw range as belonging to the native transparent queue.
|
||||||
///
|
///
|
||||||
/// The flag is deliberately independent of the blend category: native
|
/// The flag is deliberately independent of the blend category: native
|
||||||
@@ -886,14 +955,25 @@ impl VulkanSmokeRenderer {
|
|||||||
/// depth attachment switches to depth-test/read-only. The original depth
|
/// depth attachment switches to depth-test/read-only. The original depth
|
||||||
/// state is retained so disabling the flag restores the caller's state.
|
/// state is retained so disabling the flag restores the caller's state.
|
||||||
///
|
///
|
||||||
/// The transparent flag only participates in the world draw order. The
|
/// The transparent flag only participates in the world draw order. Without
|
||||||
/// backend keeps every unmarked range in its original slot, which leaves
|
/// explicit backgrounds, opaque and transparent ranges replace the
|
||||||
/// the fixed screen/sky/weather pass order intact.
|
/// existing world slots. With explicit backgrounds, opaque ranges compact
|
||||||
|
/// into those slots and the sorted transparent ranges are inserted after
|
||||||
|
/// the last background; unrelated fixed ranges keep relative order but
|
||||||
|
/// may shift slots.
|
||||||
pub fn set_draw_range_transparency(
|
pub fn set_draw_range_transparency(
|
||||||
&mut self,
|
&mut self,
|
||||||
range_index: usize,
|
range_index: usize,
|
||||||
transparent: bool,
|
transparent: bool,
|
||||||
) -> Result<(), VulkanSmokeRendererError> {
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
if self
|
||||||
|
.world_transparent_background_ranges
|
||||||
|
.contains(&range_index)
|
||||||
|
{
|
||||||
|
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
|
||||||
|
context: "world background range cannot join the world queue",
|
||||||
|
});
|
||||||
|
}
|
||||||
let base_state = *self
|
let base_state = *self
|
||||||
.draw_range_base_pipeline_states
|
.draw_range_base_pipeline_states
|
||||||
.get(range_index)
|
.get(range_index)
|
||||||
@@ -905,13 +985,7 @@ impl VulkanSmokeRenderer {
|
|||||||
context: "dynamic draw range index is out of bounds",
|
context: "dynamic draw range index is out of bounds",
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let depth = if transparent && base_state.depth != LegacyDepthMode::Disabled {
|
let next_state = apply_world_transparency(base_state, transparent);
|
||||||
LegacyDepthMode::TestReadOnly
|
|
||||||
} else {
|
|
||||||
base_state.depth
|
|
||||||
};
|
|
||||||
let mut next_state = base_state;
|
|
||||||
next_state.depth = depth;
|
|
||||||
let pipeline_changed = range.pipeline_state != next_state;
|
let pipeline_changed = range.pipeline_state != next_state;
|
||||||
range.pipeline_state = next_state;
|
range.pipeline_state = next_state;
|
||||||
// Calling this setter opts the range into the world queue. Fixed
|
// Calling this setter opts the range into the world queue. Fixed
|
||||||
@@ -1499,11 +1573,13 @@ impl VulkanSmokeRenderer {
|
|||||||
self.rebuild_swapchain_resources(true)
|
self.rebuild_swapchain_resources(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recomputes the stable command-recording order. Only ranges explicitly
|
/// Recomputes the stable command-recording order. Without explicit
|
||||||
/// marked transparent are reordered, and they remain in the slots occupied
|
/// backgrounds, opaque world ranges followed by the sorted transparent
|
||||||
/// by that queue. This preserves the caller's fixed sky/screen/weather
|
/// world ranges replace the existing world slots. With explicit
|
||||||
/// pass order while matching the native descending key comparator for the
|
/// backgrounds, opaque ranges compact into the world slots and sorted
|
||||||
/// world transparent queue.
|
/// transparent ranges are inserted after the last background; unrelated
|
||||||
|
/// fixed ranges keep relative order but may shift slots. The transparent
|
||||||
|
/// queue matches the native descending key comparator.
|
||||||
fn rebuild_draw_order(&mut self) {
|
fn rebuild_draw_order(&mut self) {
|
||||||
let range_count = self.draw_ranges.len();
|
let range_count = self.draw_ranges.len();
|
||||||
if self.draw_order.len() != range_count {
|
if self.draw_order.len() != range_count {
|
||||||
@@ -1514,6 +1590,7 @@ impl VulkanSmokeRenderer {
|
|||||||
&self.draw_ranges,
|
&self.draw_ranges,
|
||||||
&self.draw_range_world_queue,
|
&self.draw_range_world_queue,
|
||||||
&self.draw_range_sort_keys,
|
&self.draw_range_sort_keys,
|
||||||
|
&self.world_transparent_background_ranges,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2109,15 +2186,46 @@ fn apply_geometry_range_updates(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_world_transparency(
|
||||||
|
base_state: LegacyPipelineState,
|
||||||
|
transparent: bool,
|
||||||
|
) -> LegacyPipelineState {
|
||||||
|
if transparent && base_state.depth != LegacyDepthMode::Disabled {
|
||||||
|
LegacyPipelineState {
|
||||||
|
depth: LegacyDepthMode::TestReadOnly,
|
||||||
|
..base_state
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn world_material_pipeline_states(
|
||||||
|
original: LegacyPipelineState,
|
||||||
|
category: u8,
|
||||||
|
world_queue: Option<bool>,
|
||||||
|
) -> Option<(LegacyPipelineState, LegacyPipelineState)> {
|
||||||
|
let categorized = original.with_material_category(category)?;
|
||||||
|
let Some(transparent) = world_queue else {
|
||||||
|
return Some((original, original));
|
||||||
|
};
|
||||||
|
let base = categorized;
|
||||||
|
Some((base, apply_world_transparency(base, transparent)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Replays the native queue construction from the original stable range
|
/// Replays the native queue construction from the original stable range
|
||||||
/// order. Fixed passes stay in their original slots; world opaque ranges are
|
/// order. Without explicit background ranges, world opaque ranges replace the
|
||||||
/// emitted first, followed by world transparent ranges inserted by descending
|
/// existing world slots and world transparent ranges follow in descending
|
||||||
/// camera distance. Zero-index-count ranges are inactive and are omitted.
|
/// camera distance order. With explicit backgrounds, opaque ranges are
|
||||||
|
/// compacted into world positions and sorted transparent ranges are inserted
|
||||||
|
/// after the last active background; fixed passes keep their relative order but
|
||||||
|
/// can shift slots. Zero-index-count ranges are inactive and are omitted.
|
||||||
fn sorted_world_draw_order(
|
fn sorted_world_draw_order(
|
||||||
range_count: usize,
|
range_count: usize,
|
||||||
ranges: &[super::VulkanStaticDrawRange],
|
ranges: &[super::VulkanStaticDrawRange],
|
||||||
world_queue: &[Option<bool>],
|
world_queue: &[Option<bool>],
|
||||||
keys: &[Option<f32>],
|
keys: &[Option<f32>],
|
||||||
|
transparent_background_ranges: &[usize],
|
||||||
) -> Vec<usize> {
|
) -> Vec<usize> {
|
||||||
let mut next = (0..range_count).collect::<Vec<_>>();
|
let mut next = (0..range_count).collect::<Vec<_>>();
|
||||||
let mut world_opaque = Vec::new();
|
let mut world_opaque = Vec::new();
|
||||||
@@ -2176,6 +2284,7 @@ fn sorted_world_draw_order(
|
|||||||
ordered_transparent.insert(low, range_index);
|
ordered_transparent.insert(low, range_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if transparent_background_ranges.is_empty() {
|
||||||
let mut world_order = world_opaque;
|
let mut world_order = world_opaque;
|
||||||
world_order.extend(ordered_transparent);
|
world_order.extend(ordered_transparent);
|
||||||
let mut world_cursor = 0usize;
|
let mut world_cursor = 0usize;
|
||||||
@@ -2191,18 +2300,54 @@ fn sorted_world_draw_order(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed sky backgrounds are not world-queue members. Remove the world
|
||||||
|
// transparent slots, compact opaque ranges into the world slots, then
|
||||||
|
// insert the sorted transparent queue after the explicit background group.
|
||||||
|
let mut opaque_cursor = 0usize;
|
||||||
|
let mut next_without_transparency = Vec::with_capacity(range_count);
|
||||||
|
for slot in 0..range_count {
|
||||||
|
let active_world = ranges.get(slot).is_some_and(|range| range.index_count != 0);
|
||||||
|
match world_queue.get(slot).copied().flatten() {
|
||||||
|
Some(true) if active_world => {}
|
||||||
|
Some(false) if active_world => {
|
||||||
|
if let Some(&replacement) = world_opaque.get(opaque_cursor) {
|
||||||
|
next_without_transparency.push(replacement);
|
||||||
|
opaque_cursor += 1;
|
||||||
|
} else {
|
||||||
|
next_without_transparency.push(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => next_without_transparency.push(slot),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let insertion = transparent_background_ranges
|
||||||
|
.iter()
|
||||||
|
.filter_map(|background| {
|
||||||
|
next_without_transparency
|
||||||
|
.iter()
|
||||||
|
.position(|range| range == background)
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.map(|index| index + 1);
|
||||||
|
if let Some(insertion) = insertion {
|
||||||
|
next_without_transparency.splice(insertion..insertion, ordered_transparent);
|
||||||
|
}
|
||||||
|
next_without_transparency
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
|
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
|
||||||
use super::{
|
use super::{
|
||||||
apply_geometry_range_updates, completed_readback_buffer_index, sorted_world_draw_order,
|
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
|
||||||
take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
|
sorted_world_draw_order, take_runtime_children_with_validation_snapshot,
|
||||||
validate_geometry_upload, RollbackOnDrop,
|
take_runtime_owners_in_dependency_order, validate_geometry_upload,
|
||||||
|
world_material_pipeline_states, RollbackOnDrop,
|
||||||
};
|
};
|
||||||
use fparkan_render::{LegacyBlendMode, LegacyDepthMode, LegacyPipelineState};
|
use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -2506,6 +2651,7 @@ mod tests {
|
|||||||
&ranges,
|
&ranges,
|
||||||
&[None, Some(true), None, Some(true), Some(false)],
|
&[None, Some(true), None, Some(true), Some(false)],
|
||||||
&[None, Some(2.0), None, Some(9.0), Some(-3.0)],
|
&[None, Some(2.0), None, Some(9.0), Some(-3.0)],
|
||||||
|
&[],
|
||||||
);
|
);
|
||||||
assert_eq!(order, vec![0, 4, 2, 3, 1]);
|
assert_eq!(order, vec![0, 4, 2, 3, 1]);
|
||||||
}
|
}
|
||||||
@@ -2546,6 +2692,7 @@ mod tests {
|
|||||||
&ranges,
|
&ranges,
|
||||||
&[None, Some(true), Some(true)],
|
&[None, Some(true), Some(true)],
|
||||||
&[None, Some(100.0), Some(1.0)],
|
&[None, Some(100.0), Some(1.0)],
|
||||||
|
&[],
|
||||||
);
|
);
|
||||||
assert_eq!(order, vec![0, 1, 2]);
|
assert_eq!(order, vec![0, 1, 2]);
|
||||||
}
|
}
|
||||||
@@ -2563,7 +2710,99 @@ mod tests {
|
|||||||
alpha_test_reference: 0,
|
alpha_test_reference: 0,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let order = sorted_world_draw_order(4, &ranges, &[Some(true); 4], &[Some(1.0); 4]);
|
let order = sorted_world_draw_order(4, &ranges, &[Some(true); 4], &[Some(1.0); 4], &[]);
|
||||||
assert_eq!(order, vec![2, 3, 1, 0]);
|
assert_eq!(order, vec![2, 3, 1, 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tree_canopy_queue_follows_sky_and_precedes_foreground_effects() {
|
||||||
|
const SCREEN_GRADIENT: usize = 0;
|
||||||
|
const TERRAIN: usize = 1;
|
||||||
|
const CANOPY_NEAR: usize = 2;
|
||||||
|
const BUILDING: usize = 3;
|
||||||
|
const CANOPY_FAR: usize = 4;
|
||||||
|
const SHADOW: usize = 5;
|
||||||
|
const NEBULA: usize = 6;
|
||||||
|
const DOME: usize = 7;
|
||||||
|
const CLOUDS: usize = 8;
|
||||||
|
const SUN: usize = 9;
|
||||||
|
const WEATHER: usize = 10;
|
||||||
|
let ranges = (0..11)
|
||||||
|
.map(|index| VulkanStaticDrawRange {
|
||||||
|
first_index: index * 3,
|
||||||
|
index_count: 3,
|
||||||
|
material_index: 0,
|
||||||
|
lightmap_index: u8::MAX,
|
||||||
|
batch_flags: 0,
|
||||||
|
pipeline_state: LegacyPipelineState::default(),
|
||||||
|
alpha_test_reference: 0,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut world_queue = vec![None; ranges.len()];
|
||||||
|
world_queue[TERRAIN] = Some(false);
|
||||||
|
world_queue[CANOPY_NEAR] = Some(true);
|
||||||
|
world_queue[BUILDING] = Some(false);
|
||||||
|
world_queue[CANOPY_FAR] = Some(true);
|
||||||
|
let mut sort_keys = vec![None; ranges.len()];
|
||||||
|
sort_keys[CANOPY_NEAR] = Some(2.0);
|
||||||
|
sort_keys[CANOPY_FAR] = Some(9.0);
|
||||||
|
let order = sorted_world_draw_order(
|
||||||
|
ranges.len(),
|
||||||
|
&ranges,
|
||||||
|
&world_queue,
|
||||||
|
&sort_keys,
|
||||||
|
&[NEBULA, DOME, CLOUDS],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
order,
|
||||||
|
vec![
|
||||||
|
SCREEN_GRADIENT,
|
||||||
|
TERRAIN,
|
||||||
|
BUILDING,
|
||||||
|
SHADOW,
|
||||||
|
NEBULA,
|
||||||
|
DOME,
|
||||||
|
CLOUDS,
|
||||||
|
CANOPY_FAR,
|
||||||
|
CANOPY_NEAR,
|
||||||
|
SUN,
|
||||||
|
WEATHER,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn material_category_and_transparency_preserve_world_state_in_either_order() {
|
||||||
|
let original = LegacyPipelineState {
|
||||||
|
blend: LegacyBlendMode::Opaque,
|
||||||
|
depth: LegacyDepthMode::TestWrite,
|
||||||
|
cull: LegacyCullMode::FrontFace,
|
||||||
|
alpha_test: false,
|
||||||
|
};
|
||||||
|
let (base, category_first) =
|
||||||
|
world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
|
||||||
|
let transparency_first = apply_world_transparency(original, true)
|
||||||
|
.with_material_category(1)
|
||||||
|
.expect("category 1 after transparency");
|
||||||
|
assert_eq!(category_first, transparency_first);
|
||||||
|
assert_eq!(base.blend, LegacyBlendMode::SourceAlpha);
|
||||||
|
assert!(base.alpha_test);
|
||||||
|
assert_eq!(base.depth, LegacyDepthMode::TestWrite);
|
||||||
|
assert_eq!(base.cull, LegacyCullMode::FrontFace);
|
||||||
|
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
|
||||||
|
assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
|
||||||
|
|
||||||
|
let fixed_state = LegacyPipelineState {
|
||||||
|
blend: LegacyBlendMode::Additive,
|
||||||
|
depth: LegacyDepthMode::Disabled,
|
||||||
|
cull: LegacyCullMode::Disabled,
|
||||||
|
alpha_test: false,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
world_material_pipeline_states(fixed_state, 1, None),
|
||||||
|
Some((fixed_state, fixed_state))
|
||||||
|
);
|
||||||
|
assert!(world_material_pipeline_states(original, 5, Some(false)).is_none());
|
||||||
|
assert!(world_material_pipeline_states(fixed_state, 5, None).is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,6 +449,8 @@ impl VulkanStaticDrawRange {
|
|||||||
pub struct VulkanStaticMaterial {
|
pub struct VulkanStaticMaterial {
|
||||||
/// Positional material selector used by one or more source batches.
|
/// Positional material selector used by one or more source batches.
|
||||||
pub material_index: u16,
|
pub material_index: u16,
|
||||||
|
/// Native MAT0 pipeline category selected for this phase (0..=4).
|
||||||
|
pub pipeline_category: u8,
|
||||||
/// Decoded RGBA8 diffuse texture for this selector.
|
/// Decoded RGBA8 diffuse texture for this selector.
|
||||||
pub texture: VulkanStaticTexture,
|
pub texture: VulkanStaticTexture,
|
||||||
/// Phase parameters 4..6, normalized directional light RGB coefficient.
|
/// Phase parameters 4..6, normalized directional light RGB coefficient.
|
||||||
@@ -1003,6 +1005,7 @@ mod static_mesh_tests {
|
|||||||
let materials = [
|
let materials = [
|
||||||
VulkanStaticMaterial {
|
VulkanStaticMaterial {
|
||||||
material_index: 2,
|
material_index: 2,
|
||||||
|
pipeline_category: 0,
|
||||||
texture: texture(),
|
texture: texture(),
|
||||||
detail_texture: None,
|
detail_texture: None,
|
||||||
overlay_texture: None,
|
overlay_texture: None,
|
||||||
@@ -1028,6 +1031,7 @@ mod static_mesh_tests {
|
|||||||
},
|
},
|
||||||
VulkanStaticMaterial {
|
VulkanStaticMaterial {
|
||||||
material_index: 7,
|
material_index: 7,
|
||||||
|
pipeline_category: 0,
|
||||||
texture: texture(),
|
texture: texture(),
|
||||||
detail_texture: None,
|
detail_texture: None,
|
||||||
overlay_texture: None,
|
overlay_texture: None,
|
||||||
@@ -1379,6 +1383,8 @@ pub struct VulkanSmokeRenderer {
|
|||||||
/// a fixed screen/sky/weather pass; `Some(false)` is world opaque and
|
/// a fixed screen/sky/weather pass; `Some(false)` is world opaque and
|
||||||
/// `Some(true)` is world transparent.
|
/// `Some(true)` is world transparent.
|
||||||
pub(super) draw_range_world_queue: Vec<Option<bool>>,
|
pub(super) draw_range_world_queue: Vec<Option<bool>>,
|
||||||
|
/// Fixed sky background ranges placed between world opaque and transparent.
|
||||||
|
pub(super) world_transparent_background_ranges: Vec<usize>,
|
||||||
/// Pipeline state before transparent queue depth policy is applied.
|
/// Pipeline state before transparent queue depth policy is applied.
|
||||||
pub(super) draw_range_base_pipeline_states: Vec<LegacyPipelineState>,
|
pub(super) draw_range_base_pipeline_states: Vec<LegacyPipelineState>,
|
||||||
pub(super) material_alphas: Vec<[f32; 2]>,
|
pub(super) material_alphas: Vec<[f32; 2]>,
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ impl MaterialPhaseBinding {
|
|||||||
struct MaterialVariantSet {
|
struct MaterialVariantSet {
|
||||||
phase_material_indices: Vec<usize>,
|
phase_material_indices: Vec<usize>,
|
||||||
range_indices: Vec<usize>,
|
range_indices: Vec<usize>,
|
||||||
|
pipeline_category: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -676,6 +677,19 @@ struct ActiveMaterialState {
|
|||||||
diffuse_alpha: f32,
|
diffuse_alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selected_material_phase(
|
||||||
|
variant: &MaterialVariantSet,
|
||||||
|
phase_index: usize,
|
||||||
|
pipeline_category_for: impl FnOnce(usize) -> Option<u8>,
|
||||||
|
) -> Result<(usize, u8), String> {
|
||||||
|
let material_index = selected_material_variant_index(variant, phase_index)
|
||||||
|
.ok_or_else(|| "MAT0 material animation has no texture variant".to_string())?;
|
||||||
|
let pipeline_category = pipeline_category_for(material_index).ok_or_else(|| {
|
||||||
|
format!("MAT0 material animation selector {material_index} is out of bounds")
|
||||||
|
})?;
|
||||||
|
Ok((material_index, pipeline_category))
|
||||||
|
}
|
||||||
|
|
||||||
fn update_material_animations(
|
fn update_material_animations(
|
||||||
bindings: &mut [MaterialAnimationBinding],
|
bindings: &mut [MaterialAnimationBinding],
|
||||||
materials: &[VulkanStaticMaterial],
|
materials: &[VulkanStaticMaterial],
|
||||||
@@ -706,9 +720,13 @@ fn update_material_animations(
|
|||||||
.map(|overlay_detail| overlay_detail.sample(binding.initial_phase_index, clock_ms))
|
.map(|overlay_detail| overlay_detail.sample(binding.initial_phase_index, clock_ms))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let mut updated_materials = HashSet::new();
|
let mut updated_materials = HashSet::new();
|
||||||
for variant in &binding.variants {
|
for variant in &mut binding.variants {
|
||||||
let material_index = selected_material_variant_index(variant, base_sample.phase_index)
|
let (material_index, pipeline_category) =
|
||||||
.ok_or_else(|| "MAT0 material animation has no texture variant".to_string())?;
|
selected_material_phase(variant, base_sample.phase_index, |index| {
|
||||||
|
materials
|
||||||
|
.get(index)
|
||||||
|
.map(|material| material.pipeline_category)
|
||||||
|
})?;
|
||||||
for &range_index in &variant.range_indices {
|
for &range_index in &variant.range_indices {
|
||||||
active_materials.insert(
|
active_materials.insert(
|
||||||
range_index,
|
range_index,
|
||||||
@@ -720,7 +738,15 @@ fn update_material_animations(
|
|||||||
renderer
|
renderer
|
||||||
.set_draw_range_material(range_index, material_index)
|
.set_draw_range_material(range_index, material_index)
|
||||||
.map_err(|error| format!("update MAT0 material draw selector: {error}"))?;
|
.map_err(|error| format!("update MAT0 material draw selector: {error}"))?;
|
||||||
|
if variant.pipeline_category != pipeline_category {
|
||||||
|
renderer
|
||||||
|
.set_draw_range_material_category(range_index, pipeline_category)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("update MAT0 material pipeline category: {error}")
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
variant.pipeline_category = pipeline_category;
|
||||||
if !updated_materials.insert(material_index) {
|
if !updated_materials.insert(material_index) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -887,6 +913,37 @@ fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) -
|
|||||||
diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 0
|
diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn configure_world_material_categories(
|
||||||
|
mesh: &mut VulkanStaticMesh,
|
||||||
|
materials: &[VulkanStaticMaterial],
|
||||||
|
world_range_indices: &[usize],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for &range_index in world_range_indices {
|
||||||
|
let range = mesh
|
||||||
|
.draw_ranges
|
||||||
|
.get_mut(range_index)
|
||||||
|
.ok_or_else(|| "world material range index is out of bounds".to_string())?;
|
||||||
|
let material = materials
|
||||||
|
.get(usize::from(range.material_index))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"world material selector {} is out of bounds",
|
||||||
|
range.material_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
range.pipeline_state = range
|
||||||
|
.pipeline_state
|
||||||
|
.with_material_category(material.pipeline_category)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"world material selector {} has unsupported native pipeline category {}",
|
||||||
|
range.material_index, material.pipeline_category
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn register_material_range(
|
fn register_material_range(
|
||||||
bindings: &mut [MaterialAnimationBinding],
|
bindings: &mut [MaterialAnimationBinding],
|
||||||
material_index: usize,
|
material_index: usize,
|
||||||
@@ -1164,6 +1221,7 @@ fn append_environment_material(
|
|||||||
})?;
|
})?;
|
||||||
materials.push(VulkanStaticMaterial {
|
materials.push(VulkanStaticMaterial {
|
||||||
material_index,
|
material_index,
|
||||||
|
pipeline_category: phase.pipeline_category,
|
||||||
texture: asset
|
texture: asset
|
||||||
.phase_textures
|
.phase_textures
|
||||||
.get(phase_index)
|
.get(phase_index)
|
||||||
@@ -1201,6 +1259,7 @@ fn append_environment_material(
|
|||||||
variants: vec![MaterialVariantSet {
|
variants: vec![MaterialVariantSet {
|
||||||
phase_material_indices,
|
phase_material_indices,
|
||||||
range_indices: Vec::new(),
|
range_indices: Vec::new(),
|
||||||
|
pipeline_category: phases[0].pipeline_category,
|
||||||
}],
|
}],
|
||||||
base: MaterialPhaseBinding {
|
base: MaterialPhaseBinding {
|
||||||
document,
|
document,
|
||||||
@@ -1228,6 +1287,7 @@ fn append_environment_gradient_material(
|
|||||||
})?;
|
})?;
|
||||||
materials.push(VulkanStaticMaterial {
|
materials.push(VulkanStaticMaterial {
|
||||||
material_index,
|
material_index,
|
||||||
|
pipeline_category: 0,
|
||||||
texture: solid_environment_texture([255, 255, 255, 255]),
|
texture: solid_environment_texture([255, 255, 255, 255]),
|
||||||
directional_rgb: [1.0; 3],
|
directional_rgb: [1.0; 3],
|
||||||
additive_rgb: [0.0; 3],
|
additive_rgb: [0.0; 3],
|
||||||
@@ -1275,6 +1335,7 @@ fn append_shadow_material(materials: &mut Vec<VulkanStaticMaterial>) -> Result<u
|
|||||||
let index = materials.len();
|
let index = materials.len();
|
||||||
materials.push(VulkanStaticMaterial {
|
materials.push(VulkanStaticMaterial {
|
||||||
material_index,
|
material_index,
|
||||||
|
pipeline_category: 0,
|
||||||
texture: shadow_atlas_texture(),
|
texture: shadow_atlas_texture(),
|
||||||
directional_rgb: [1.0; 3],
|
directional_rgb: [1.0; 3],
|
||||||
additive_rgb: [0.0; 3],
|
additive_rgb: [0.0; 3],
|
||||||
@@ -1859,6 +1920,19 @@ fn screen_gradient_quad_vertices(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EnvironmentGpuScene {
|
impl EnvironmentGpuScene {
|
||||||
|
fn transparent_background_range_indices(&self) -> Vec<usize> {
|
||||||
|
self.sky_layers
|
||||||
|
.iter()
|
||||||
|
.filter(|layer| {
|
||||||
|
matches!(
|
||||||
|
layer.kind,
|
||||||
|
SkyPassKind::NebulaStars | SkyPassKind::DomeGradient | SkyPassKind::Clouds
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|layer| layer.range.range_index)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn fixed_range_indices(&self) -> HashSet<usize> {
|
fn fixed_range_indices(&self) -> HashSet<usize> {
|
||||||
self.sky_layers
|
self.sky_layers
|
||||||
.iter()
|
.iter()
|
||||||
@@ -2289,9 +2363,9 @@ impl EnvironmentGpuScene {
|
|||||||
&& *index != cloud_index
|
&& *index != cloud_index
|
||||||
}));
|
}));
|
||||||
old_order.push(*shadow_range_index);
|
old_order.push(*shadow_range_index);
|
||||||
old_order.extend([nebula_index, dome_index]);
|
old_order.extend([nebula_index, dome_index, cloud_index]);
|
||||||
old_order.extend(sprite_indices.iter().copied());
|
old_order.extend(sprite_indices.iter().copied());
|
||||||
old_order.extend([cloud_index, rain_index, snow_index, lightning_index]);
|
old_order.extend([rain_index, snow_index, lightning_index]);
|
||||||
let remap = reorder_draw_ranges(mesh, &old_order)?;
|
let remap = reorder_draw_ranges(mesh, &old_order)?;
|
||||||
for layer in &mut scene.sky_layers {
|
for layer in &mut scene.sky_layers {
|
||||||
layer.range.range_index = *remap
|
layer.range.range_index = *remap
|
||||||
@@ -4165,6 +4239,7 @@ fn static_preview_terrain_base_materials(
|
|||||||
})?;
|
})?;
|
||||||
materials.push(VulkanStaticMaterial {
|
materials.push(VulkanStaticMaterial {
|
||||||
material_index: preview_selector,
|
material_index: preview_selector,
|
||||||
|
pipeline_category: base_phase.pipeline_category,
|
||||||
texture: base.clone(),
|
texture: base.clone(),
|
||||||
detail_texture: Some(detail.clone()),
|
detail_texture: Some(detail.clone()),
|
||||||
overlay_texture: overlay.map(|(texture, _)| texture.clone()),
|
overlay_texture: overlay.map(|(texture, _)| texture.clone()),
|
||||||
@@ -4257,6 +4332,7 @@ fn static_preview_terrain_base_materials(
|
|||||||
variants: vec![MaterialVariantSet {
|
variants: vec![MaterialVariantSet {
|
||||||
phase_material_indices,
|
phase_material_indices,
|
||||||
range_indices: Vec::new(),
|
range_indices: Vec::new(),
|
||||||
|
pipeline_category: base_phases[initial_phase_index].1.pipeline_category,
|
||||||
}],
|
}],
|
||||||
base: MaterialPhaseBinding {
|
base: MaterialPhaseBinding {
|
||||||
document: base_document,
|
document: base_document,
|
||||||
@@ -4369,6 +4445,7 @@ fn static_preview_component_materials(
|
|||||||
})?;
|
})?;
|
||||||
materials.push(VulkanStaticMaterial {
|
materials.push(VulkanStaticMaterial {
|
||||||
material_index: preview_selector,
|
material_index: preview_selector,
|
||||||
|
pipeline_category: material.pipeline_category,
|
||||||
texture,
|
texture,
|
||||||
detail_texture: None,
|
detail_texture: None,
|
||||||
overlay_texture: None,
|
overlay_texture: None,
|
||||||
@@ -4405,6 +4482,7 @@ fn static_preview_component_materials(
|
|||||||
variants: vec![MaterialVariantSet {
|
variants: vec![MaterialVariantSet {
|
||||||
phase_material_indices,
|
phase_material_indices,
|
||||||
range_indices: Vec::new(),
|
range_indices: Vec::new(),
|
||||||
|
pipeline_category: material.pipeline_category,
|
||||||
}],
|
}],
|
||||||
base: MaterialPhaseBinding {
|
base: MaterialPhaseBinding {
|
||||||
document: material.mat0.clone(),
|
document: material.mat0.clone(),
|
||||||
@@ -4551,6 +4629,7 @@ fn append_static_preview_component_with_lightmaps(
|
|||||||
"static preview material selector {source_material_index} has no MAT0 animation binding"
|
"static preview material selector {source_material_index} has no MAT0 animation binding"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let initial_phase_index = bindings[binding_index].initial_phase_index;
|
||||||
let lightmap = lightmap_textures
|
let lightmap = lightmap_textures
|
||||||
.get(usize::from(range.lightmap_index))
|
.get(usize::from(range.lightmap_index))
|
||||||
.expect("lightmap presence checked above");
|
.expect("lightmap presence checked above");
|
||||||
@@ -4590,6 +4669,16 @@ fn append_static_preview_component_with_lightmaps(
|
|||||||
materials.push(lightmap_material);
|
materials.push(lightmap_material);
|
||||||
phase_material_indices.push(usize::from(index));
|
phase_material_indices.push(usize::from(index));
|
||||||
}
|
}
|
||||||
|
let initial_selector = phase_material_indices
|
||||||
|
.get(initial_phase_index)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| "static preview lightmap phase table is empty".to_string())?;
|
||||||
|
let pipeline_category = materials
|
||||||
|
.get(initial_selector)
|
||||||
|
.map(|material| material.pipeline_category)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
"static preview lightmap material selector is unavailable".to_string()
|
||||||
|
})?;
|
||||||
let bindings = material_animations.as_deref_mut().ok_or_else(|| {
|
let bindings = material_animations.as_deref_mut().ok_or_else(|| {
|
||||||
"static preview lightmap animation storage is unavailable".to_string()
|
"static preview lightmap animation storage is unavailable".to_string()
|
||||||
})?;
|
})?;
|
||||||
@@ -4597,6 +4686,7 @@ fn append_static_preview_component_with_lightmaps(
|
|||||||
bindings[binding_index].variants.push(MaterialVariantSet {
|
bindings[binding_index].variants.push(MaterialVariantSet {
|
||||||
phase_material_indices: phase_material_indices.clone(),
|
phase_material_indices: phase_material_indices.clone(),
|
||||||
range_indices: Vec::new(),
|
range_indices: Vec::new(),
|
||||||
|
pipeline_category,
|
||||||
});
|
});
|
||||||
let value = (phase_material_indices, binding_index, variant_index);
|
let value = (phase_material_indices, binding_index, variant_index);
|
||||||
lightmap_materials.insert(key, value.clone());
|
lightmap_materials.insert(key, value.clone());
|
||||||
@@ -4913,6 +5003,16 @@ impl StaticVulkanApp {
|
|||||||
renderer
|
renderer
|
||||||
.set_draw_range_transparency(range_index, transparent)
|
.set_draw_range_transparency(range_index, transparent)
|
||||||
.map_err(|error| format!("configure world draw range {range_index}: {error}"))?;
|
.map_err(|error| format!("configure world draw range {range_index}: {error}"))?;
|
||||||
|
let category = self
|
||||||
|
.materials
|
||||||
|
.get(usize::from(range.material_index))
|
||||||
|
.ok_or_else(|| "world draw range has no material selector".to_string())?
|
||||||
|
.pipeline_category;
|
||||||
|
renderer
|
||||||
|
.set_draw_range_material_category(range_index, category)
|
||||||
|
.map_err(|error| {
|
||||||
|
format!("configure world material category {range_index}: {error}")
|
||||||
|
})?;
|
||||||
renderer
|
renderer
|
||||||
.set_draw_range_index_count(
|
.set_draw_range_index_count(
|
||||||
range_index,
|
range_index,
|
||||||
@@ -5381,6 +5481,15 @@ impl ApplicationHandler for StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let gpu_initialization_started = Instant::now();
|
let gpu_initialization_started = Instant::now();
|
||||||
|
if let Err(error) = configure_world_material_categories(
|
||||||
|
&mut self.mesh,
|
||||||
|
&self.materials,
|
||||||
|
&self.world_range_indices,
|
||||||
|
) {
|
||||||
|
self.error = Some(error);
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut renderer = match VulkanSmokeRenderer::new(&VulkanSmokeRendererCreateInfo {
|
let mut renderer = match VulkanSmokeRenderer::new(&VulkanSmokeRendererCreateInfo {
|
||||||
application_name: "fparkan-game".to_string(),
|
application_name: "fparkan-game".to_string(),
|
||||||
native_handles,
|
native_handles,
|
||||||
@@ -5399,6 +5508,15 @@ impl ApplicationHandler for StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let background_ranges = self.environment_gpu.as_ref().map_or_else(
|
||||||
|
Vec::new,
|
||||||
|
EnvironmentGpuScene::transparent_background_range_indices,
|
||||||
|
);
|
||||||
|
if let Err(error) = renderer.set_world_transparent_background_ranges(&background_ranges) {
|
||||||
|
self.error = Some(format!("configure world background ranges: {error}"));
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Err(error) = self.configure_world_draw_ranges(&mut renderer) {
|
if let Err(error) = self.configure_world_draw_ranges(&mut renderer) {
|
||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
@@ -5975,6 +6093,7 @@ mod tests {
|
|||||||
variants: vec![MaterialVariantSet {
|
variants: vec![MaterialVariantSet {
|
||||||
phase_material_indices: vec![10],
|
phase_material_indices: vec![10],
|
||||||
range_indices: vec![0, 2],
|
range_indices: vec![0, 2],
|
||||||
|
pipeline_category: 0,
|
||||||
}],
|
}],
|
||||||
base: empty_binding(),
|
base: empty_binding(),
|
||||||
detail: None,
|
detail: None,
|
||||||
@@ -6029,6 +6148,30 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn material_animation_selects_pipeline_category_with_active_phase() {
|
||||||
|
let variant = MaterialVariantSet {
|
||||||
|
phase_material_indices: vec![10, 11],
|
||||||
|
range_indices: vec![3],
|
||||||
|
pipeline_category: 1,
|
||||||
|
};
|
||||||
|
let category_for = |material_index| match material_index {
|
||||||
|
10 => Some(1),
|
||||||
|
11 => Some(2),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
selected_material_phase(&variant, 0, category_for),
|
||||||
|
Ok((10, 1))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
selected_material_phase(&variant, 1, category_for),
|
||||||
|
Ok((11, 2))
|
||||||
|
);
|
||||||
|
assert!(selected_material_phase(&variant, 2, category_for).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_sun_object_source_types_map_to_world_kinds() {
|
fn native_sun_object_source_types_map_to_world_kinds() {
|
||||||
assert_eq!(native_shadow_kind(Some(PROTOTYPE_TYPE_FORT)), Some(3));
|
assert_eq!(native_shadow_kind(Some(PROTOTYPE_TYPE_FORT)), Some(3));
|
||||||
@@ -6187,6 +6330,7 @@ mod tests {
|
|||||||
variants: vec![MaterialVariantSet {
|
variants: vec![MaterialVariantSet {
|
||||||
phase_material_indices: vec![10, 11],
|
phase_material_indices: vec![10, 11],
|
||||||
range_indices: Vec::new(),
|
range_indices: Vec::new(),
|
||||||
|
pipeline_category: 0,
|
||||||
}],
|
}],
|
||||||
base: MaterialPhaseBinding {
|
base: MaterialPhaseBinding {
|
||||||
document: empty_mat0,
|
document: empty_mat0,
|
||||||
@@ -6207,6 +6351,7 @@ mod tests {
|
|||||||
bindings[binding_index].variants.push(MaterialVariantSet {
|
bindings[binding_index].variants.push(MaterialVariantSet {
|
||||||
phase_material_indices: vec![20, 21],
|
phase_material_indices: vec![20, 21],
|
||||||
range_indices: vec![7],
|
range_indices: vec![7],
|
||||||
|
pipeline_category: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
let lightmap_variant = &bindings[binding_index].variants[1];
|
let lightmap_variant = &bindings[binding_index].variants[1];
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ struct Phase34 {
|
|||||||
|
|
||||||
### Pipeline category
|
### Pipeline category
|
||||||
|
|
||||||
Call site извлекает native category как `(attr1 >> 2) & 0xF` и передаёт её в
|
Call site извлекает native category как `(attr1 >> 2) & 0xF` и сохраняет её на
|
||||||
`LegacyPipelineState::with_material_category`. Таблица содержит пять
|
разрешённом phase material. World draw ranges получают состояние для начального
|
||||||
подтверждённых категорий; helper возвращает `None` для остальных значений и не
|
selector при подготовке, а animated selector обновляет category вместе с
|
||||||
меняет уже выбранные depth и cull state.
|
material selector. Fixed environment ranges keep their authored pipeline state.
|
||||||
|
Таблица содержит пять подтверждённых категорий; helper возвращает `None` для
|
||||||
|
остальных значений и не меняет уже выбранные depth и cull state.
|
||||||
|
|
||||||
| category | source/destination colour factors | alpha test |
|
| category | source/destination colour factors | alpha test |
|
||||||
| ---: | --- | --- |
|
| ---: | --- | --- |
|
||||||
@@ -75,6 +77,13 @@ material category. Alpha reference remains draw-range data, while the Vulkan
|
|||||||
pipeline key includes the blend mode, depth mode, cull mode and alpha-test
|
pipeline key includes the blend mode, depth mode, cull mode and alpha-test
|
||||||
variant so ranges with different native state do not share a pipeline.
|
variant so ranges with different native state do not share a pipeline.
|
||||||
|
|
||||||
|
World transparency is a separate draw-queue decision. A world range joins that
|
||||||
|
queue when sampled diffuse opacity is below one or any bit in the native
|
||||||
|
`0x108` batch-flag mask is set; the MAT0 category alone does not make it
|
||||||
|
transparent. A queued range
|
||||||
|
with a depth attachment tests depth without writing it. Unsupported categories
|
||||||
|
fail world-range setup instead of silently falling back to opaque state.
|
||||||
|
|
||||||
Каждая phase занимает 34 байта: 18 parameter bytes и 16-byte C string имени
|
Каждая phase занимает 34 байта: 18 parameter bytes и 16-byte C string имени
|
||||||
текстуры. Loader переводит параметры в коэффициенты так:
|
текстуры. Loader переводит параметры в коэффициенты так:
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,47 @@ end-of-render.
|
|||||||
Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase
|
Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase
|
||||||
или texture cache посреди прохода не должно менять уже собранную очередь.
|
или texture cache посреди прохода не должно менять уже собранную очередь.
|
||||||
|
|
||||||
|
## MAT0 state and transparent backgrounds
|
||||||
|
|
||||||
|
For mission world ranges, the selected MAT0 material supplies its native
|
||||||
|
pipeline category. The category sets blend and alpha-test state while preserving
|
||||||
|
the range's depth and cull state; an animated material selector updates the
|
||||||
|
category with the selector. Queue membership is independent: sampled opacity
|
||||||
|
below one or native batch flags `0x108` mark a world range transparent, and a
|
||||||
|
queued range with depth enabled tests depth without writing it. Fixed
|
||||||
|
environment ranges keep their explicit states.
|
||||||
|
|
||||||
|
The fixed-camera preview explicitly marks nebula/stars, dome and cloud ranges as
|
||||||
|
transparent backgrounds. Opaque world ranges stay in their world slots; the
|
||||||
|
transparent world queue is sorted back-to-front and inserted immediately after
|
||||||
|
the last marked background. Other fixed ranges retain their relative order.
|
||||||
|
Thus cloud layers can be drawn before transparent foliage, while later sun,
|
||||||
|
weather and other foreground ranges keep their existing relative order. This
|
||||||
|
is a narrow queue rule, not a reordering of every sky or effects pass.
|
||||||
|
|
||||||
|
## Regression evidence
|
||||||
|
|
||||||
|
`cargo test -p fparkan-render-vulkan --lib material_category_and_transparency_preserve_world_state_in_either_order --offline`
|
||||||
|
checks that category 1 remains source-alpha with alpha testing, retains depth
|
||||||
|
and cull settings in either update order, and rejects unsupported categories.
|
||||||
|
`cargo test -p fparkan-render-vulkan --lib tree_canopy_queue_follows_sky_and_precedes_foreground_effects --offline`
|
||||||
|
checks the explicit queue order: screen gradient, opaque terrain/building,
|
||||||
|
shadow, nebula/dome/clouds, far-to-near transparent canopy, then sun/weather.
|
||||||
|
|
||||||
|
Viewer-only paired readbacks used `--frames 3 --validation
|
||||||
|
--legacy-camera-capture` with the pre-fix and patched builds. On AutoDemo TMA
|
||||||
|
object 4 (`wor_l_n1.dat`, `fr_l_plant`), the 1280x720 camera at eye
|
||||||
|
`[464.319, 904.719, 38.0]`, aimed at `[374.319, 814.719, 12.0]` with FOV
|
||||||
|
`0.62`, changed the two black facade panels to tinted, textured portal
|
||||||
|
surfaces. On Mission.01 object 12 (`s_tree_04`), the 1280x1024 camera at eye
|
||||||
|
`[492.63855, 312.90244, 46.5]` aimed at
|
||||||
|
`[492.63855, 442.90244, 55.0]` crosses the canopy against open sky; paired
|
||||||
|
three-frame captures at the same synthetic atmosphere phase (600 seconds) show
|
||||||
|
the patched build restoring the missing upper/right fronds along that sky
|
||||||
|
boundary. Both runs reported clean Vulkan validation. These captures verify our
|
||||||
|
preview behavior and do not prove the native renderer's pass order, portal
|
||||||
|
transmittance or exact color.
|
||||||
|
|
||||||
## GOG camera dispatch evidence
|
## GOG camera dispatch evidence
|
||||||
|
|
||||||
For the GOG `World3D.dll` baseline with SHA-256
|
For the GOG `World3D.dll` baseline with SHA-256
|
||||||
|
|||||||
@@ -275,14 +275,17 @@ buffers и передаёт собранную settings-структуру да
|
|||||||
`ret 0x0c`; `CreateCollManager` и `CreateCollObject` возвращают interface
|
`ret 0x0c`; `CreateCollManager` и `CreateCollObject` возвращают interface
|
||||||
pointer с поправкой `+4` после внутренней инициализации.
|
pointer с поправкой `+4` после внутренней инициализации.
|
||||||
|
|
||||||
Headless Ghidra 12.1.2 decompile GOG binary подтверждает ABI формой экспортов
|
Инструкциям x86 в GOG `Control.dll` (`+0x32280`) и caller-у в `AniMesh.dll`
|
||||||
`LoadControlSystem(char*, char*, char*, char*, char*, u32, void*, i32)`,
|
(`+0x32e7`) соответствует ABI `LoadControlSystem(char*, char*, char*, char*,
|
||||||
|
char*, char*, void*, i32)`: шесть C-string pointers, resource context и mode.
|
||||||
|
Функция заканчивается `ret 0x20`, то есть снимает все восемь 32-bit stack
|
||||||
|
arguments; arg7 передаётся constructor-у как context, arg8 выбирает mode.
|
||||||
|
Прежняя декомпиляция ошибочно определяла шестой строковый аргумент как `u32`.
|
||||||
|
Остальные exports имеют формы
|
||||||
`LoadPhysicalModel(u32, u32, u32)`, `CreateCollManager(u32)` и
|
`LoadPhysicalModel(u32, u32, u32)`, `CreateCollManager(u32)` и
|
||||||
`CreateCollObject(u32, u32)`. Первые пять параметров Control loader — строки,
|
`CreateCollObject(u32, u32)`. `InitializeSettings` получает
|
||||||
а mode передаётся последним; decompiler не восстанавливает предметные имена
|
`CreateGameSettings()` из World3D и делает virtual call slot `+0x24` с literal
|
||||||
остальных слов. `InitializeSettings` получает `CreateGameSettings()` из
|
`0x15` и строкой по RVA `0x42478`.
|
||||||
World3D и делает virtual call slot `+0x24` с literal `0x15` и строкой по RVA
|
|
||||||
`0x42478`.
|
|
||||||
|
|
||||||
Именно update methods этих private objects, а не пять exports, остаются
|
Именно update methods этих private objects, а не пять exports, остаются
|
||||||
следующим объектом динамической трассировки. Поэтому reference movement в
|
следующим объектом динамической трассировки. Поэтому reference movement в
|
||||||
|
|||||||
Reference in New Issue
Block a user