aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan')
-rw-r--r--adapters/fparkan-render-vulkan/shaders/triangle.vert9
-rw-r--r--adapters/fparkan-render-vulkan/shaders/triangle.vert.spvbin18580 -> 19136 bytes
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs179
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs23
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs72
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/tests.rs2
6 files changed, 215 insertions, 70 deletions
diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert b/adapters/fparkan-render-vulkan/shaders/triangle.vert
index 201c31a..83a1751 100644
--- a/adapters/fparkan-render-vulkan/shaders/triangle.vert
+++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert
@@ -232,9 +232,10 @@ void main() {
out_uv = in_uv;
out_detail_uv = in_detail_uv;
out_overlay_alpha = in_overlay_alpha;
- out_fog_factor = native_fog_factor(in_position);
int mode = int(round(material.material_mode));
int combiner_mode = mode & 3;
+ bool screen_tl = (mode & 8) != 0;
+ out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);
if (combiner_mode == 0) {
LightingTerms base_terms = native_material_lighting(
in_position,
@@ -262,12 +263,14 @@ void main() {
out_overlay_diffuse = vec3(1.0);
out_overlay_specular = vec3(0.0);
}
- vec4 clip_position = frame.clip_from_world * vec4(in_position, 1.0);
+ vec4 clip_position = screen_tl
+ ? vec4(in_position, 1.0)
+ : frame.clip_from_world * vec4(in_position, 1.0);
// Sky far-depth is a Vulkan projection choice for layers authored around
// the camera. Keep world positions, normals, and UVs intact while placing
// the resulting primitive at the far depth; this does not assert native
// D3D depth-bit equivalence.
- if ((mode & 4) != 0) {
+ if ((mode & 4) != 0 && !screen_tl) {
clip_position.z = clip_position.w;
}
gl_Position = clip_position;
diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
index 9574e1c..535bde0 100644
--- a/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
+++ b/adapters/fparkan-render-vulkan/shaders/triangle.vert.spv
Binary files differ
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
index 9d084ae..0de1381 100644
--- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs
+++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
@@ -7,7 +7,7 @@ use fparkan_msh::{
ModelAsset, NodeId,
};
use fparkan_render::{LegacyDepthMode, LegacyIron3dEulerTransform, LegacyPipelineState};
-use fparkan_terrain_format::LandMeshDocument;
+use fparkan_terrain_format::{LandMeshDocument, TerrainFaceLod, TerrainLod, TerrainLodError};
fn world_pipeline_state() -> LegacyPipelineState {
LegacyPipelineState {
@@ -34,6 +34,8 @@ pub enum VulkanAssetMeshError {
/// Number of supplied node poses.
actual: usize,
},
+ /// Terrain Node38/Slot68 records do not assign every face to one LOD.
+ InvalidTerrainLodMapping(TerrainLodError),
}
/// Evaluated world pose of one standard Node38 node in model-local space.
@@ -108,6 +110,9 @@ impl std::fmt::Display for VulkanAssetMeshError {
f,
"node pose buffer has {actual} poses, but the MSH has {expected} nodes"
),
+ Self::InvalidTerrainLodMapping(error) => {
+ write!(f, "invalid terrain LOD mapping: {error}")
+ }
}
}
}
@@ -1016,23 +1021,15 @@ fn transform_normal(
pub fn project_land_msh_to_static_mesh(
terrain: &LandMeshDocument,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
- let mut indices = Vec::with_capacity(
- terrain
- .faces
- .len()
- .checked_mul(3)
- .ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
- );
- for face in &terrain.faces {
- indices.extend(face.vertices.map(u32::from));
- }
+ let lod0 = static_terrain_lod0_assignments(terrain)?;
+ let indices = static_terrain_indices(terrain, &lod0)?;
if indices.is_empty() {
return Err(VulkanAssetMeshError::EmptyGeometry);
}
let (min_x, max_x, min_y, max_y) = static_mesh_xy_bounds(&terrain.positions, &indices)?;
let frame = VulkanStaticXyFrame::from_bounds(min_x, max_x, min_y, max_y)?;
- project_land_msh_to_static_mesh_in_xy_frame(terrain, frame)
+ project_land_msh_to_static_mesh_with_lod0(terrain, frame, &lod0)
}
/// Projects `Land.msh` terrain geometry into a shared diagnostic XY frame.
@@ -1045,16 +1042,16 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame(
terrain: &LandMeshDocument,
frame: VulkanStaticXyFrame,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
- let mut indices = Vec::with_capacity(
- terrain
- .faces
- .len()
- .checked_mul(3)
- .ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
- );
- for face in &terrain.faces {
- indices.extend(face.vertices.map(u32::from));
- }
+ let lod0 = static_terrain_lod0_assignments(terrain)?;
+ project_land_msh_to_static_mesh_with_lod0(terrain, frame, &lod0)
+}
+
+fn project_land_msh_to_static_mesh_with_lod0(
+ terrain: &LandMeshDocument,
+ frame: VulkanStaticXyFrame,
+ lod0: &[TerrainFaceLod],
+) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
+ let indices = static_terrain_indices(terrain, lod0)?;
if indices.is_empty() {
return Err(VulkanAssetMeshError::EmptyGeometry);
}
@@ -1089,7 +1086,7 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame(
Ok(VulkanStaticMesh {
vertices,
indices,
- draw_ranges: static_terrain_draw_ranges(terrain)?,
+ draw_ranges: static_terrain_draw_ranges(terrain, lod0)?,
})
}
@@ -1107,7 +1104,8 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame(
pub fn project_land_msh_to_static_mesh_in_world_space(
terrain: &LandMeshDocument,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
- let indices = static_terrain_indices(terrain)?;
+ let lod0 = static_terrain_lod0_assignments(terrain)?;
+ let indices = static_terrain_indices(terrain, &lod0)?;
let vertices = terrain
.positions
.iter()
@@ -1132,19 +1130,34 @@ pub fn project_land_msh_to_static_mesh_in_world_space(
Ok(VulkanStaticMesh {
vertices,
indices,
- draw_ranges: static_terrain_draw_ranges(terrain)?,
+ draw_ranges: static_terrain_draw_ranges(terrain, &lod0)?,
})
}
-fn static_terrain_indices(terrain: &LandMeshDocument) -> Result<Vec<u32>, VulkanAssetMeshError> {
+fn static_terrain_lod0_assignments(
+ terrain: &LandMeshDocument,
+) -> Result<Vec<TerrainFaceLod>, VulkanAssetMeshError> {
+ let mut assignments = terrain
+ .face_lod_assignments()
+ .map_err(VulkanAssetMeshError::InvalidTerrainLodMapping)?;
+ assignments.retain(|assignment| assignment.lod == TerrainLod::Lod0);
+ Ok(assignments)
+}
+
+fn static_terrain_indices(
+ terrain: &LandMeshDocument,
+ lod0: &[TerrainFaceLod],
+) -> Result<Vec<u32>, VulkanAssetMeshError> {
let mut indices = Vec::with_capacity(
- terrain
- .faces
- .len()
+ lod0.len()
.checked_mul(3)
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
);
- for face in &terrain.faces {
+ for assignment in lod0 {
+ let face = terrain
+ .faces
+ .get(assignment.face_index)
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
indices.extend(face.vertices.map(u32::from));
}
if indices.is_empty() {
@@ -1187,14 +1200,24 @@ fn terrain_overlay_alpha(terrain: &LandMeshDocument, index: usize) -> f32 {
fn static_terrain_draw_ranges(
terrain: &LandMeshDocument,
+ lod0: &[TerrainFaceLod],
) -> Result<Vec<VulkanStaticDrawRange>, VulkanAssetMeshError> {
let mut ranges = Vec::new();
let mut start_face = 0usize;
- while start_face < terrain.faces.len() {
- let material_index = terrain.faces[start_face].material_tag;
+ while start_face < lod0.len() {
+ let material_index = terrain
+ .faces
+ .get(lod0[start_face].face_index)
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?
+ .material_tag;
let mut end_face = start_face + 1;
- while end_face < terrain.faces.len()
- && terrain.faces[end_face].material_tag == material_index
+ while end_face < lod0.len()
+ && terrain
+ .faces
+ .get(lod0[end_face].face_index)
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?
+ .material_tag
+ == material_index
{
end_face += 1;
}
@@ -1998,7 +2021,7 @@ mod tests {
#[test]
fn projects_land_faces_in_source_order_with_packed_uv0() {
- let terrain = LandMeshDocument {
+ let mut terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
@@ -2018,6 +2041,7 @@ mod tests {
aux18: Vec::new(),
faces: vec![terrain_face([0, 1, 2]), terrain_face([1, 3, 2])],
};
+ set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 2]]);
let mesh = project_land_msh_to_static_mesh(&terrain).expect("representable terrain");
@@ -2031,8 +2055,60 @@ mod tests {
}
#[test]
+ fn terrain_projection_filters_interleaved_lods_in_source_face_order() {
+ let mut terrain = LandMeshDocument {
+ streams: Vec::new(),
+ nodes_raw: Vec::new(),
+ slots: TerrainSlotTable {
+ header_raw: Vec::new(),
+ slots_raw: Vec::new(),
+ },
+ positions: vec![
+ [0.0, 0.0, 0.0],
+ [1.0, 0.0, 0.0],
+ [0.0, 1.0, 0.0],
+ [10.0, 10.0, 10.0],
+ [11.0, 10.0, 10.0],
+ [10.0, 11.0, 10.0],
+ ],
+ normals: Vec::new(),
+ uv0: Vec::new(),
+ accelerator: Vec::new(),
+ aux14: Vec::new(),
+ aux18: Vec::new(),
+ faces: vec![
+ terrain_face_with_tag(0x0101, [0, 1, 2]),
+ terrain_face_with_tag(0x0202, [3, 4, 5]),
+ terrain_face_with_tag(0x0101, [0, 1, 2]),
+ terrain_face_with_tag(0x0202, [3, 4, 5]),
+ ],
+ };
+ set_terrain_lod_slots(
+ &mut terrain,
+ &[[0, 1], [2, 3]],
+ &[[0, 1], [1, 1], [2, 1], [3, 1]],
+ );
+
+ let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain)
+ .expect("LOD0 terrain projection");
+ assert_eq!(mesh.indices, [0, 1, 2, 0, 1, 2]);
+ assert_eq!(mesh.draw_ranges.len(), 1);
+ assert_eq!(mesh.draw_ranges[0].first_index, 0);
+ assert_eq!(mesh.draw_ranges[0].index_count, 6);
+ assert_eq!(mesh.draw_ranges[0].material_index, 0x0101);
+
+ terrain.nodes_raw.clear();
+ assert!(matches!(
+ project_land_msh_to_static_mesh_in_world_space(&terrain),
+ Err(VulkanAssetMeshError::InvalidTerrainLodMapping(
+ TerrainLodError::MissingNodes { face_count: 4 }
+ ))
+ ));
+ }
+
+ #[test]
fn terrain_normals_keep_native_one_over_128_scale() {
- let terrain = LandMeshDocument {
+ let mut terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
@@ -2047,6 +2123,7 @@ mod tests {
aux18: Vec::new(),
faces: vec![terrain_face([0, 1, 2])],
};
+ set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 1]]);
let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain)
.expect("representable terrain normals");
@@ -2055,7 +2132,7 @@ mod tests {
#[test]
fn world_space_terrain_preserves_nonzero_source_heights_and_faces() {
- let terrain = LandMeshDocument {
+ let mut terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
@@ -2074,6 +2151,7 @@ mod tests {
aux18: Vec::new(),
faces: vec![terrain_face([2, 0, 1])],
};
+ set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 1]]);
let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain)
.expect("representable source-space terrain");
@@ -2101,9 +2179,30 @@ mod tests {
}
}
+ fn set_terrain_lod_slots(
+ terrain: &mut LandMeshDocument,
+ node_slots: &[[u16; 2]],
+ face_ranges: &[[u16; 2]],
+ ) {
+ const NODE_STRIDE: usize = 38;
+ let mut nodes = vec![u8::MAX; node_slots.len() * NODE_STRIDE];
+ for (node_index, [lod0, lod1]) in node_slots.iter().copied().enumerate() {
+ let offset = node_index * NODE_STRIDE + 8;
+ nodes[offset..offset + 2].copy_from_slice(&lod0.to_le_bytes());
+ nodes[offset + 2..offset + 4].copy_from_slice(&lod1.to_le_bytes());
+ }
+ let mut slots = vec![[0_u8; 68]; face_ranges.len()];
+ for (slot, [start, count]) in slots.iter_mut().zip(face_ranges.iter().copied()) {
+ slot[..2].copy_from_slice(&start.to_le_bytes());
+ slot[2..4].copy_from_slice(&count.to_le_bytes());
+ }
+ terrain.nodes_raw = nodes;
+ terrain.slots.slots_raw = slots;
+ }
+
#[test]
fn terrain_ranges_preserve_face_order_and_packed_material_tags() {
- let terrain = LandMeshDocument {
+ let mut terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
@@ -2123,8 +2222,10 @@ mod tests {
terrain_face_with_tag(0xff01, [0, 1, 2]),
],
};
+ set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]);
- let ranges = static_terrain_draw_ranges(&terrain).expect("representable terrain");
+ let lod0 = static_terrain_lod0_assignments(&terrain).expect("terrain LOD0");
+ let ranges = static_terrain_draw_ranges(&terrain, &lod0).expect("representable terrain");
assert_eq!(ranges.len(), 3);
assert_eq!(ranges[0].first_index, 0);
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index c45c331..ac359c2 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -3,7 +3,7 @@
use ash::vk;
use fparkan_render::{LegacyDepthMode, LegacyPipelineState};
-use super::smoke_types::resolve_draw_texture_indices;
+use super::smoke_types::{resolve_draw_texture_indices, VulkanStaticMaterial};
use super::update_material_texture_descriptor;
use super::{
create_command_pool, create_frame_sync, create_index_buffer, create_static_mesh_index_buffer,
@@ -361,18 +361,7 @@ impl VulkanSmokeRenderer {
create_info
.materials
.iter()
- .map(|material| {
- let combiner_mode = if material.lightmap_mode {
- 3.0
- } else if material.sky_nebula_stars {
- 2.0
- } else if material.unlit {
- 1.0
- } else {
- 0.0
- };
- combiner_mode + if material.sky_far_depth { 4.0 } else { 0.0 }
- })
+ .map(VulkanStaticMaterial::encoded_material_mode)
.collect()
};
let material_specular = if create_info.materials.is_empty() {
@@ -1091,10 +1080,10 @@ impl VulkanSmokeRenderer {
.chain(additive_rgb)
.chain(overlay_directional_rgb.into_iter().flatten())
.chain(overlay_additive_rgb.into_iter().flatten())
- .any(|value| !value.is_finite() || value < 0.0)
+ .any(|value| !value.is_finite())
{
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
- context: "dynamic material lighting must be finite and non-negative",
+ context: "dynamic material lighting must be finite",
});
}
let lighting = self.material_lighting.get_mut(material_index).ok_or(
@@ -1155,10 +1144,10 @@ impl VulkanSmokeRenderer {
if specular_rgb
.into_iter()
.chain(overlay_specular_rgb.into_iter().flatten())
- .any(|value| !value.is_finite() || value < 0.0)
+ .any(|value| !value.is_finite())
{
return Err(VulkanSmokeRendererError::InvalidStaticMesh {
- context: "dynamic material specular RGB must be finite and non-negative",
+ context: "dynamic material specular RGB must be finite",
});
}
let specular = self.material_specular.get_mut(material_index).ok_or(
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
index c41eff9..7a8837d 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs
@@ -505,6 +505,33 @@ pub struct VulkanStaticMaterial {
/// renderer projection choice; it is not a claim about the original D3D
/// depth-bit encoding.
pub sky_far_depth: bool,
+ /// Push material mode bit 8 and treat positions as Vulkan clip-space
+ /// `XYZRHW` vertices. Their x/y coordinates are converted from viewport
+ /// pixels by the caller and z retains the native `0.999` depth.
+ pub screen_tl: bool,
+}
+
+impl VulkanStaticMaterial {
+ pub(crate) fn encoded_material_mode(&self) -> f32 {
+ let combiner_mode = if self.screen_tl {
+ 1.0
+ } else if self.lightmap_mode {
+ 3.0
+ } else if self.sky_nebula_stars {
+ 2.0
+ } else if self.unlit {
+ 1.0
+ } else {
+ 0.0
+ };
+ combiner_mode
+ + if self.sky_far_depth && !self.screen_tl {
+ 4.0
+ } else {
+ 0.0
+ }
+ + if self.screen_tl { 8.0 } else { 0.0 }
+ }
}
/// One authored RGBA8 mip level accepted by the Vulkan texture upload path.
@@ -622,9 +649,9 @@ pub(super) fn resolve_draw_texture_indices(
.chain(material.additive_rgb)
.chain(material.specular_rgb)
.chain(material.overlay_specular_rgb.into_iter().flatten())
- .any(|value| !value.is_finite() || value < 0.0)
+ .any(|value| !value.is_finite())
{
- return Err("static material lighting and specular RGB parameters must be finite and non-negative");
+ return Err("static material lighting and specular RGB parameters must be finite");
}
if material
.uv_transform
@@ -645,9 +672,9 @@ pub(super) fn resolve_draw_texture_indices(
.into_iter()
.flatten()
.chain(material.overlay_additive_rgb.into_iter().flatten())
- .any(|value| !value.is_finite() || value < 0.0)
+ .any(|value| !value.is_finite())
{
- return Err("static material overlay RGB parameters must be finite and non-negative");
+ return Err("static material overlay RGB parameters must be finite");
}
if material
.overlay_uv_transform
@@ -1012,15 +1039,15 @@ mod static_mesh_tests {
overlay_detail_texture: None,
diffuse_alpha: 1.0,
overlay_diffuse_alpha: None,
- directional_rgb: [1.0; 3],
- additive_rgb: [0.0; 3],
- specular_rgb: [0.0; 3],
+ directional_rgb: [-0.25; 3],
+ additive_rgb: [-0.5; 3],
+ specular_rgb: [-0.75; 3],
specular_power: 0,
- overlay_specular_rgb: None,
+ overlay_specular_rgb: Some([-1.0; 3]),
overlay_specular_power: None,
uv_transform: [0.0, 0.0, 1.0, 1.0],
- overlay_directional_rgb: None,
- overlay_additive_rgb: None,
+ overlay_directional_rgb: Some([-0.125; 3]),
+ overlay_additive_rgb: Some([-0.375; 3]),
detail_uv_transform: [0.0, 0.0, 1.0, 1.0],
overlay_uv_transform: None,
overlay_detail_uv_transform: None,
@@ -1028,6 +1055,7 @@ mod static_mesh_tests {
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
+ screen_tl: false,
},
VulkanStaticMaterial {
material_index: 7,
@@ -1054,13 +1082,37 @@ mod static_mesh_tests {
sky_nebula_stars: false,
lightmap_mode: false,
sky_far_depth: false,
+ screen_tl: false,
},
];
+ let mut screen_tl = materials[0].clone();
+ screen_tl.screen_tl = true;
+ screen_tl.sky_far_depth = true;
+ assert_eq!(screen_tl.encoded_material_mode(), 9.0);
+ let mut far_sky = materials[0].clone();
+ far_sky.unlit = true;
+ far_sky.sky_far_depth = true;
+ assert_eq!(far_sky.encoded_material_mode(), 5.0);
+
assert_eq!(
resolve_draw_texture_indices(&ranges, &materials),
Ok(vec![1, 0])
);
+
+ let mut non_finite_base = materials.clone();
+ non_finite_base[0].specular_rgb[0] = f32::NAN;
+ assert_eq!(
+ resolve_draw_texture_indices(&ranges, &non_finite_base),
+ Err("static material lighting and specular RGB parameters must be finite")
+ );
+
+ let mut non_finite_overlay = materials.clone();
+ non_finite_overlay[0].overlay_additive_rgb = Some([f32::INFINITY, 0.0, 0.0]);
+ assert_eq!(
+ resolve_draw_texture_indices(&ranges, &non_finite_overlay),
+ Err("static material overlay RGB parameters must be finite")
+ );
}
#[test]
diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
index b8a3d71..4ed61f7 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs
@@ -533,7 +533,7 @@ fn native_specular_reference_uses_squared_power_and_total_overflow_curve() {
fn shader_computes_radial_fog_per_vertex_and_interpolates_it() {
let vertex = include_str!("../../shaders/triangle.vert");
let fragment = include_str!("../../shaders/triangle.frag");
- assert!(vertex.contains("out_fog_factor = native_fog_factor(in_position);"));
+ assert!(vertex.contains("out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);"));
assert!(fragment.contains("layout(location = 4) in float in_fog_factor;"));
assert!(!fragment.contains("distance(in_world_position"));
assert!(!fragment.contains("float fog_factor()"));