feat(render): select static MAT0 material phases
This commit is contained in:
Generated
+1
@@ -448,6 +448,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fparkan-assets",
|
"fparkan-assets",
|
||||||
"fparkan-inspection",
|
"fparkan-inspection",
|
||||||
|
"fparkan-path",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-platform-winit",
|
"fparkan-platform-winit",
|
||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ repository.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" }
|
fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" }
|
||||||
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" }
|
||||||
|
fparkan-path = { path = "../../crates/fparkan-path", version = "0.1.0" }
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
use fparkan_assets::{PreparedTextureUsage, PreparedVisual};
|
use fparkan_assets::{PreparedTextureUsage, PreparedVisual};
|
||||||
use fparkan_inspection::load_standalone_wear_material_texture_mip0_rgba8_from_root;
|
use fparkan_inspection::load_standalone_wear_material_texture_mip0_rgba8_from_root;
|
||||||
|
use fparkan_path::ResourceName;
|
||||||
use fparkan_platform::WindowPort;
|
use fparkan_platform::WindowPort;
|
||||||
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
@@ -113,6 +114,7 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
roots,
|
roots,
|
||||||
camera,
|
camera,
|
||||||
args.static_animation_frame,
|
args.static_animation_frame,
|
||||||
|
args.static_material_phase,
|
||||||
&args.root,
|
&args.root,
|
||||||
&loaded.land_msh_path,
|
&loaded.land_msh_path,
|
||||||
)?;
|
)?;
|
||||||
@@ -245,6 +247,7 @@ struct StaticPreviewScene {
|
|||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
terrain_components: usize,
|
terrain_components: usize,
|
||||||
animation_frame: Option<u16>,
|
animation_frame: Option<u16>,
|
||||||
|
material_phase: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Projects the mission terrain plus every MSH component of the explicitly
|
/// Projects the mission terrain plus every MSH component of the explicitly
|
||||||
@@ -256,13 +259,14 @@ struct StaticPreviewScene {
|
|||||||
/// retained on draw ranges but awaits a recovered blend equation; orientation,
|
/// retained on draw ranges but awaits a recovered blend equation; orientation,
|
||||||
/// later material phases, animation, lightmaps and gameplay visibility remain
|
/// later material phases, animation, lightmaps and gameplay visibility remain
|
||||||
/// outside this bridge.
|
/// outside this bridge.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||||
fn static_preview_mesh_and_materials(
|
fn static_preview_mesh_and_materials(
|
||||||
assets: &MissionAssets,
|
assets: &MissionAssets,
|
||||||
terrain: &TerrainWorld,
|
terrain: &TerrainWorld,
|
||||||
roots: &[MissionObjectDraft],
|
roots: &[MissionObjectDraft],
|
||||||
legacy_camera: Option<VulkanStaticCamera>,
|
legacy_camera: Option<VulkanStaticCamera>,
|
||||||
static_animation_frame: Option<u16>,
|
static_animation_frame: Option<u16>,
|
||||||
|
static_material_phase: Option<u16>,
|
||||||
root: &std::path::Path,
|
root: &std::path::Path,
|
||||||
land_msh_path: &str,
|
land_msh_path: &str,
|
||||||
) -> Result<StaticPreviewScene, String> {
|
) -> Result<StaticPreviewScene, String> {
|
||||||
@@ -346,8 +350,13 @@ fn static_preview_mesh_and_materials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
||||||
let selector_remap =
|
let selector_remap = static_preview_component_materials(
|
||||||
static_preview_component_materials(assets, visual, &component, &mut materials)?;
|
assets,
|
||||||
|
visual,
|
||||||
|
&component,
|
||||||
|
static_material_phase,
|
||||||
|
&mut materials,
|
||||||
|
)?;
|
||||||
append_static_preview_component(&mut mesh, component, &selector_remap)?;
|
append_static_preview_component(&mut mesh, component, &selector_remap)?;
|
||||||
mesh_components += 1;
|
mesh_components += 1;
|
||||||
}
|
}
|
||||||
@@ -369,6 +378,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
mesh_components,
|
mesh_components,
|
||||||
terrain_components: 1,
|
terrain_components: 1,
|
||||||
animation_frame: static_animation_frame,
|
animation_frame: static_animation_frame,
|
||||||
|
material_phase: static_material_phase,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +549,7 @@ fn static_preview_component_materials(
|
|||||||
assets: &MissionAssets,
|
assets: &MissionAssets,
|
||||||
visual: &PreparedVisual,
|
visual: &PreparedVisual,
|
||||||
mesh: &VulkanStaticMesh,
|
mesh: &VulkanStaticMesh,
|
||||||
|
static_material_phase: Option<u16>,
|
||||||
materials: &mut Vec<VulkanStaticMaterial>,
|
materials: &mut Vec<VulkanStaticMaterial>,
|
||||||
) -> Result<Vec<(u16, u16)>, String> {
|
) -> Result<Vec<(u16, u16)>, String> {
|
||||||
let mut source_selectors = mesh
|
let mut source_selectors = mesh
|
||||||
@@ -562,15 +573,21 @@ fn static_preview_component_materials(
|
|||||||
let material = assets.material_by_id(*material_id).ok_or_else(|| {
|
let material = assets.material_by_id(*material_id).ok_or_else(|| {
|
||||||
format!("static preview prepared material {source_selector} is unavailable")
|
format!("static preview prepared material {source_selector} is unavailable")
|
||||||
})?;
|
})?;
|
||||||
let texture_name = material.texture_requests.first().ok_or_else(|| {
|
let texture_name = static_material_phase
|
||||||
format!("static preview material {source_selector} has no MAT0 diffuse texture")
|
.and_then(|phase_index| {
|
||||||
})?;
|
let phase = material.mat0.phases.get(usize::from(phase_index))?;
|
||||||
|
let length = phase.texture_raw.iter().position(|byte| *byte == 0).unwrap_or(phase.texture_raw.len());
|
||||||
|
(length != 0).then(|| ResourceName(phase.texture_raw[..length].to_vec()))
|
||||||
|
})
|
||||||
|
.or_else(|| material.texture_requests.first().cloned())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("static preview material {source_selector} has no MAT0 diffuse texture")
|
||||||
|
})?;
|
||||||
let texture = assets
|
let texture = assets
|
||||||
.textures
|
.textures
|
||||||
.iter()
|
.iter()
|
||||||
.find(|texture| {
|
.find(|texture| {
|
||||||
texture.usage == PreparedTextureUsage::Diffuse
|
texture.usage == PreparedTextureUsage::Diffuse && texture.source.name == texture_name
|
||||||
&& texture.source.name == *texture_name
|
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -703,6 +720,7 @@ struct StaticVulkanApp {
|
|||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
terrain_components: usize,
|
terrain_components: usize,
|
||||||
animation_frame: Option<u16>,
|
animation_frame: Option<u16>,
|
||||||
|
material_phase: Option<u16>,
|
||||||
preview_roots: usize,
|
preview_roots: usize,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: String,
|
mission: String,
|
||||||
@@ -734,6 +752,7 @@ impl StaticVulkanApp {
|
|||||||
mesh_components: preview.mesh_components,
|
mesh_components: preview.mesh_components,
|
||||||
terrain_components: preview.terrain_components,
|
terrain_components: preview.terrain_components,
|
||||||
animation_frame: preview.animation_frame,
|
animation_frame: preview.animation_frame,
|
||||||
|
material_phase: preview.material_phase,
|
||||||
preview_roots,
|
preview_roots,
|
||||||
target_frames,
|
target_frames,
|
||||||
mission: mission.to_string(),
|
mission: mission.to_string(),
|
||||||
@@ -802,13 +821,14 @@ impl StaticVulkanApp {
|
|||||||
(None, _) => None,
|
(None, _) => None,
|
||||||
};
|
};
|
||||||
self.output = Some(format!(
|
self.output = Some(format!(
|
||||||
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"camera_mode\":{},\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"preview_roots\":{},\"animation_frame\":{},\"mesh_components\":{},\"terrain_components\":{},\"clip_visible_vertices\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_format\":{},\"readback_bytes\":{},\"readback_hash\":{},\"readback_path\":{}}}",
|
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"camera_mode\":{},\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"preview_roots\":{},\"animation_frame\":{},\"material_phase\":{},\"mesh_components\":{},\"terrain_components\":{},\"clip_visible_vertices\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_format\":{},\"readback_bytes\":{},\"readback_hash\":{},\"readback_path\":{}}}",
|
||||||
json_string(self.camera_mode),
|
json_string(self.camera_mode),
|
||||||
json_string(&self.mission),
|
json_string(&self.mission),
|
||||||
self.object_count,
|
self.object_count,
|
||||||
self.frames_presented,
|
self.frames_presented,
|
||||||
self.preview_roots,
|
self.preview_roots,
|
||||||
self.animation_frame.map_or_else(|| "null".to_string(), |frame| frame.to_string()),
|
self.animation_frame.map_or_else(|| "null".to_string(), |frame| frame.to_string()),
|
||||||
|
self.material_phase.map_or_else(|| "null".to_string(), |phase| phase.to_string()),
|
||||||
self.mesh_components,
|
self.mesh_components,
|
||||||
self.terrain_components,
|
self.terrain_components,
|
||||||
self.clip_visible_vertices,
|
self.clip_visible_vertices,
|
||||||
@@ -1063,6 +1083,7 @@ struct Args {
|
|||||||
preview_roots: NonZeroUsize,
|
preview_roots: NonZeroUsize,
|
||||||
legacy_camera_capture: Option<PathBuf>,
|
legacy_camera_capture: Option<PathBuf>,
|
||||||
static_animation_frame: Option<u16>,
|
static_animation_frame: Option<u16>,
|
||||||
|
static_material_phase: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -1087,6 +1108,7 @@ impl Args {
|
|||||||
let mut preview_roots = NonZeroUsize::MAX;
|
let mut preview_roots = NonZeroUsize::MAX;
|
||||||
let mut legacy_camera_capture = None;
|
let mut legacy_camera_capture = None;
|
||||||
let mut static_animation_frame = None;
|
let mut static_animation_frame = None;
|
||||||
|
let mut static_material_phase = None;
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
@@ -1159,6 +1181,16 @@ impl Args {
|
|||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
"--static-material-phase" => {
|
||||||
|
static_material_phase = Some(
|
||||||
|
iter.next()
|
||||||
|
.ok_or_else(|| "--static-material-phase requires a value".to_string())?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| {
|
||||||
|
"--static-material-phase must be a u16 integer".to_string()
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => return Err(usage()),
|
_ => return Err(usage()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1176,6 +1208,9 @@ impl Args {
|
|||||||
if static_animation_frame.is_some() && backend != RenderBackendMode::StaticVulkan {
|
if static_animation_frame.is_some() && backend != RenderBackendMode::StaticVulkan {
|
||||||
return Err("--static-animation-frame requires --backend static-vulkan".to_string());
|
return Err("--static-animation-frame requires --backend static-vulkan".to_string());
|
||||||
}
|
}
|
||||||
|
if static_material_phase.is_some() && backend != RenderBackendMode::StaticVulkan {
|
||||||
|
return Err("--static-material-phase requires --backend static-vulkan".to_string());
|
||||||
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root,
|
root,
|
||||||
mission,
|
mission,
|
||||||
@@ -1186,6 +1221,7 @@ impl Args {
|
|||||||
preview_roots,
|
preview_roots,
|
||||||
legacy_camera_capture,
|
legacy_camera_capture,
|
||||||
static_animation_frame,
|
static_animation_frame,
|
||||||
|
static_material_phase,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1293,7 @@ fn json_hash(hash: &[u8; 32]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: fparkan-game --root <path> --mission <path> [--frames <n>] [--backend <planning|static-vulkan>] [--preview-roots <non-zero n>] [--legacy-camera-capture <path>] [--static-animation-frame <u16>] [--readback-out <path>] [--load-progress <path>]".to_string()
|
"usage: fparkan-game --root <path> --mission <path> [--frames <n>] [--backend <planning|static-vulkan>] [--preview-roots <non-zero n>] [--legacy-camera-capture <path>] [--static-animation-frame <u16>] [--static-material-phase <u16>] [--readback-out <path>] [--load-progress <path>]".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1326,6 +1362,7 @@ mod tests {
|
|||||||
preview_roots: NonZeroUsize::MAX,
|
preview_roots: NonZeroUsize::MAX,
|
||||||
legacy_camera_capture: None,
|
legacy_camera_capture: None,
|
||||||
static_animation_frame: None,
|
static_animation_frame: None,
|
||||||
|
static_material_phase: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1388,7 @@ mod tests {
|
|||||||
preview_roots: NonZeroUsize::MAX,
|
preview_roots: NonZeroUsize::MAX,
|
||||||
legacy_camera_capture: None,
|
legacy_camera_capture: None,
|
||||||
static_animation_frame: None,
|
static_animation_frame: None,
|
||||||
|
static_material_phase: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1468,34 @@ mod tests {
|
|||||||
assert_eq!(parsed.static_animation_frame, Some(12));
|
assert_eq!(parsed.static_animation_frame, Some(12));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_material_phase_requires_static_vulkan_and_is_retained() {
|
||||||
|
let common = [
|
||||||
|
"--root",
|
||||||
|
"testdata/IS",
|
||||||
|
"--mission",
|
||||||
|
"MISSIONS/Autodemo.00/data.tma",
|
||||||
|
"--static-material-phase",
|
||||||
|
"2",
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
Args::parse(&strings(&common)),
|
||||||
|
Err("--static-material-phase requires --backend static-vulkan".to_string())
|
||||||
|
);
|
||||||
|
let parsed = Args::parse(&strings(&[
|
||||||
|
"--root",
|
||||||
|
"testdata/IS",
|
||||||
|
"--mission",
|
||||||
|
"MISSIONS/Autodemo.00/data.tma",
|
||||||
|
"--backend",
|
||||||
|
"static-vulkan",
|
||||||
|
"--static-material-phase",
|
||||||
|
"2",
|
||||||
|
]))
|
||||||
|
.expect("valid static material phase");
|
||||||
|
assert_eq!(parsed.static_material_phase, Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_zero_static_preview_root_count() {
|
fn rejects_zero_static_preview_root_count() {
|
||||||
let error = Args::parse(&strings(&[
|
let error = Args::parse(&strings(&[
|
||||||
@@ -1467,6 +1533,7 @@ mod tests {
|
|||||||
preview_roots: NonZeroUsize::MAX,
|
preview_roots: NonZeroUsize::MAX,
|
||||||
legacy_camera_capture: None,
|
legacy_camera_capture: None,
|
||||||
static_animation_frame: None,
|
static_animation_frame: None,
|
||||||
|
static_material_phase: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1895,3 +1895,19 @@ the licensed GOG `MISSIONS/Autodemo.00/data.tma` run with all eight roots,
|
|||||||
`--static-animation-frame 1`, two frames and format `50` completed with 66
|
`--static-animation-frame 1`, two frames and format `50` completed with 66
|
||||||
mesh components, 75,543 clip-visible vertices, a 1280×720 / 3,686,400-byte
|
mesh components, 75,543 clip-visible vertices, a 1280×720 / 3,686,400-byte
|
||||||
readback hash `4638497144561211935`, and validation warnings/errors `0/0`.
|
readback hash `4638497144561211935`, and validation warnings/errors `0/0`.
|
||||||
|
|
||||||
|
### Explicit MAT0 phase preview
|
||||||
|
|
||||||
|
`fparkan-game --backend static-vulkan --static-material-phase <u16>` now
|
||||||
|
selects the exact decoded MAT0 texture for that phase of each MSH material.
|
||||||
|
The override is deliberately per material: when the requested phase is absent
|
||||||
|
or intentionally untextured, that material retains its existing first prepared
|
||||||
|
diffuse request, rather than fabricating a texture or aborting the whole
|
||||||
|
mission. Terrain remains on its separately proven Land2 base path. The JSON
|
||||||
|
report records `material_phase` so the artifact's selector is explicit. This
|
||||||
|
is an asset-viewer control, not a claim about the original material clock,
|
||||||
|
phase mode, interpolation, random offset, lighting or Land1 composition.
|
||||||
|
|
||||||
|
On 2026-07-18 the licensed GOG AutoDemo run with all eight roots, phase `1`,
|
||||||
|
two frames and format `50` completed validation-clean with 71 descriptors and
|
||||||
|
a 1280×720 / 3,686,400-byte readback hash `3152437928708207395`.
|
||||||
|
|||||||
Reference in New Issue
Block a user