diff --git a/Cargo.lock b/Cargo.lock index 7fee04c00..6c9b89b6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,7 +80,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.5.5" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=88fdb556dc8a292931a52b52ba42fc322dd39c23#88fdb556dc8a292931a52b52ba42fc322dd39c23" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=6f3711809e39e9ffdf16a63086079b6475d97175#6f3711809e39e9ffdf16a63086079b6475d97175" dependencies = [ "ahash 0.8.12", "anyhow", @@ -895,7 +895,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a3185be8189d78d6e93b4ea5b9238b184da4f3cf#a3185be8189d78d6e93b4ea5b9238b184da4f3cf" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=935d90b86a99377dc95cba92e99a9b109180e2f6#935d90b86a99377dc95cba92e99a9b109180e2f6" dependencies = [ "acadrust", "cavalier_contours", @@ -906,7 +906,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a3185be8189d78d6e93b4ea5b9238b184da4f3cf#a3185be8189d78d6e93b4ea5b9238b184da4f3cf" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=935d90b86a99377dc95cba92e99a9b109180e2f6#935d90b86a99377dc95cba92e99a9b109180e2f6" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index e4d968f87..d87fae446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,9 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a3185be8189d78d6e93b4ea5b9238b184da4f3cf", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a3185be8189d78d6e93b4ea5b9238b184da4f3cf" } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "935d90b86a99377dc95cba92e99a9b109180e2f6", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "935d90b86a99377dc95cba92e99a9b109180e2f6" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index a765a5f14..81abdb539 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -38,7 +38,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index f6f2b1c04..b1810ed6b 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/crates/plugin-template-api2/Cargo.toml b/crates/plugin-template-api2/Cargo.toml index 23295ea22..13e0d62ac 100644 --- a/crates/plugin-template-api2/Cargo.toml +++ b/crates/plugin-template-api2/Cargo.toml @@ -10,4 +10,4 @@ crate-type = ["cdylib"] [dependencies] ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] } -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } diff --git a/docs/plugin-template-v2/Cargo.toml b/docs/plugin-template-v2/Cargo.toml index c3d7b49b6..4ee65b486 100644 --- a/docs/plugin-template-v2/Cargo.toml +++ b/docs/plugin-template-v2/Cargo.toml @@ -12,4 +12,4 @@ crate-type = ["cdylib"] [dependencies] ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] } -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "88fdb556dc8a292931a52b52ba42fc322dd39c23" } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175" } diff --git a/src/scene/model/material_model.rs b/src/scene/model/material_model.rs index f1c5e1af6..316550ee7 100644 --- a/src/scene/model/material_model.rs +++ b/src/scene/model/material_model.rs @@ -1,4 +1,5 @@ use acadrust::objects::{Material, MaterialColor, MaterialMap, ObjectType}; +use acadrust::xdata::XDataValue; use acadrust::{CadDocument, EntityType, Handle}; use std::path::Path; use std::sync::Arc; @@ -39,6 +40,7 @@ pub struct MeshMaterial { pub final_gather: i16, pub color_bleed_scale: f32, pub advanced_data_present: bool, + pub mapper: Option, pub diffuse_map: MeshTextureMap, pub specular_map: MeshTextureMap, pub reflection_map: MeshTextureMap, @@ -48,6 +50,82 @@ pub struct MeshMaterial { pub normal_map: MeshTextureMap, } +#[derive(Clone, Copy, Debug)] +pub struct MeshMaterialMapper { + pub origin: [f64; 3], + pub inverse_basis: [[f64; 3]; 3], + pub normal_basis: [[f64; 3]; 3], +} + +impl MeshMaterialMapper { + pub fn map_position(&self, high: [f32; 3], low: [f32; 3]) -> [f32; 3] { + let point = [ + high[0] as f64 + low[0] as f64 - self.origin[0], + high[1] as f64 + low[1] as f64 - self.origin[1], + high[2] as f64 + low[2] as f64 - self.origin[2], + ]; + [ + (self.inverse_basis[0][0] * point[0] + + self.inverse_basis[0][1] * point[1] + + self.inverse_basis[0][2] * point[2]) as f32, + (self.inverse_basis[1][0] * point[0] + + self.inverse_basis[1][1] * point[1] + + self.inverse_basis[1][2] * point[2]) as f32, + (self.inverse_basis[2][0] * point[0] + + self.inverse_basis[2][1] * point[1] + + self.inverse_basis[2][2] * point[2]) as f32, + ] + } + + pub fn map_normal(&self, normal: [f32; 3]) -> [f32; 3] { + let mapped = [ + self.normal_basis[0][0] * normal[0] as f64 + + self.normal_basis[0][1] * normal[1] as f64 + + self.normal_basis[0][2] * normal[2] as f64, + self.normal_basis[1][0] * normal[0] as f64 + + self.normal_basis[1][1] * normal[1] as f64 + + self.normal_basis[1][2] * normal[2] as f64, + self.normal_basis[2][0] * normal[0] as f64 + + self.normal_basis[2][1] * normal[1] as f64 + + self.normal_basis[2][2] * normal[2] as f64, + ]; + let length = + (mapped[0] * mapped[0] + mapped[1] * mapped[1] + mapped[2] * mapped[2]).sqrt(); + if length > f64::EPSILON { + [ + (mapped[0] / length) as f32, + (mapped[1] / length) as f32, + (mapped[2] / length) as f32, + ] + } else { + normal + } + } + + pub fn map_bounds(&self, bounds: [f32; 6]) -> [f32; 6] { + let mut mapped = [ + f32::INFINITY, + f32::INFINITY, + f32::INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + ]; + for x in [bounds[0], bounds[3]] { + for y in [bounds[1], bounds[4]] { + for z in [bounds[2], bounds[5]] { + let point = self.map_position([x, y, z], [0.0; 3]); + for axis in 0..3 { + mapped[axis] = mapped[axis].min(point[axis]); + mapped[axis + 3] = mapped[axis + 3].max(point[axis]); + } + } + } + } + mapped + } +} + #[derive(Clone, Debug)] pub struct MeshTextureMap { pub blend_factor: f32, @@ -146,6 +224,7 @@ impl MeshMaterial { final_gather: 0, color_bleed_scale: 1.0, advanced_data_present: false, + mapper: None, diffuse_map: MeshTextureMap::default(), specular_map: MeshTextureMap::default(), reflection_map: MeshTextureMap::default(), @@ -210,6 +289,7 @@ impl MeshMaterial { final_gather: material.final_gather, color_bleed_scale: material.color_bleed_scale as f32, advanced_data_present: material.advanced_data_present, + mapper: None, diffuse_map, specular_map, reflection_map, @@ -283,7 +363,7 @@ fn procedural_map_image(map: &MaterialMap) -> Option> { let mut rgba = Vec::with_capacity((size * size * 4) as usize); for y in 0..size { for x in 0..size { - let color = if ((x / 8) + (y / 8)) & 1 == 0 { + let color = if (x < size / 2) == (y < size / 2) { color1 } else { color2 @@ -315,9 +395,11 @@ pub fn resolve_material_with_base( ) -> MeshMaterial { let common = entity.common(); if common.material_flags == 1 { - return by_block + let mut material = by_block .cloned() .unwrap_or_else(|| MeshMaterial::entity_color(entity_color)); + material.mapper = entity_material_mapper(entity); + return material; } let handle = if common.material_flags == 3 { common.material_handle @@ -331,7 +413,7 @@ pub fn resolve_material_with_base( let Some(handle) = handle else { return MeshMaterial::entity_color(entity_color); }; - match document.objects.get(&handle) { + let mut material = match document.objects.get(&handle) { Some(ObjectType::Material(material)) if material.name.eq_ignore_ascii_case("ByLayer") || material.name.eq_ignore_ascii_case("ByBlock") => @@ -342,7 +424,58 @@ pub fn resolve_material_with_base( MeshMaterial::from_dwg(handle, material, entity_color, base_dir) } _ => MeshMaterial::entity_color(entity_color), + }; + material.mapper = entity_material_mapper(entity); + material +} + +fn entity_material_mapper(entity: &EntityType) -> Option { + let record = entity + .common() + .extended_data + .get_record("ACAD_MATERIAL_MAPPER")?; + let mut positions = record.values.iter().filter_map(|value| match value { + XDataValue::Position3D(point) => Some([point.x, point.y, point.z]), + _ => None, + }); + let origin = positions.next()?; + let x = positions.next()?; + let y = positions.next()?; + let z = positions.next()?; + let basis = glam::DMat3::from_cols( + glam::DVec3::from_array([ + x[0] - origin[0], + x[1] - origin[1], + x[2] - origin[2], + ]), + glam::DVec3::from_array([ + y[0] - origin[0], + y[1] - origin[1], + y[2] - origin[2], + ]), + glam::DVec3::from_array([ + z[0] - origin[0], + z[1] - origin[1], + z[2] - origin[2], + ]), + ); + if basis.determinant().abs() <= f64::EPSILON { + return None; } + let inverse = basis.inverse().to_cols_array(); + Some(MeshMaterialMapper { + origin, + inverse_basis: [ + [inverse[0], inverse[3], inverse[6]], + [inverse[1], inverse[4], inverse[7]], + [inverse[2], inverse[5], inverse[8]], + ], + normal_basis: [ + [x[0] - origin[0], x[1] - origin[1], x[2] - origin[2]], + [y[0] - origin[0], y[1] - origin[1], y[2] - origin[2]], + [z[0] - origin[0], z[1] - origin[1], z[2] - origin[2]], + ], + }) } pub fn resolve_layer_material_with_base( @@ -384,7 +517,9 @@ pub fn resolve_material_handle_with_base( fallback.clone() } Some(ObjectType::Material(material)) => { - MeshMaterial::from_dwg(handle, material, fallback.diffuse, base_dir) + let mut material = MeshMaterial::from_dwg(handle, material, fallback.diffuse, base_dir); + material.mapper = fallback.mapper; + material } _ => fallback.clone(), } @@ -442,3 +577,56 @@ fn load_map_image(map: &MaterialMap, base_dir: Option<&Path>) -> Option) -> Option> { None } + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::entities::Line; + use acadrust::objects::MaterialTexture; + use acadrust::types::Vector3; + use acadrust::xdata::{ExtendedDataRecord, XDataValue}; + + #[test] + fn procedural_checker_is_one_repeatable_two_by_two_tile() { + let mut map = MaterialMap::default(); + map.texture = Some(MaterialTexture { + color1: MaterialColor { + flag: 1, + factor: 1.0, + rgb: Some(0xFF0000), + }, + color2: MaterialColor { + flag: 1, + factor: 1.0, + rgb: Some(0x00FF00), + }, + ..MaterialTexture::default() + }); + + let image = procedural_map_image(&map).expect("procedural image"); + let pixel = |x: usize, y: usize| &image.rgba[(y * 64 + x) * 4..][..4]; + assert_eq!(pixel(0, 0), [255, 0, 0, 255]); + assert_eq!(pixel(32, 0), [0, 255, 0, 255]); + assert_eq!(pixel(0, 32), [0, 255, 0, 255]); + assert_eq!(pixel(32, 32), [255, 0, 0, 255]); + } + + #[test] + fn entity_material_mapper_preserves_its_coordinate_frame() { + let mut line = Line::new(); + let mut record = ExtendedDataRecord::new("ACAD_MATERIAL_MAPPER"); + record.values = vec![ + XDataValue::Integer16(2), + XDataValue::Integer16(0), + XDataValue::Integer16(0), + XDataValue::Position3D(Vector3::new(10.0, 20.0, 30.0)), + XDataValue::Position3D(Vector3::new(10.0, 22.0, 30.0)), + XDataValue::Position3D(Vector3::new(7.0, 20.0, 30.0)), + XDataValue::Position3D(Vector3::new(10.0, 20.0, 34.0)), + ]; + line.common.extended_data.add_record(record); + let mapper = entity_material_mapper(&EntityType::Line(line)).expect("valid mapper"); + + assert_eq!(mapper.map_position([4.0, 22.0, 42.0], [0.0; 3]), [1.0, 2.0, 3.0]); + } +} diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index e5c0ee788..084b1deb9 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -1037,9 +1037,28 @@ fn material_key( key[34] = material.final_gather as u32; key[35] = material.color_bleed_scale.to_bits(); key[36] = material.advanced_data_present as u32; + let mapper = material_mapper_key(material); + key[37] = mapper as u32; + key[38] = (mapper >> 32) as u32; + key[39] = material.mapper.is_some() as u32; MaterialBatchKey(key) } +fn material_mapper_key( + material: &crate::scene::model::material_model::MeshMaterial, +) -> u64 { + use std::hash::{Hash, Hasher}; + let Some(mapper) = material.mapper else { + return 0; + }; + let mut hasher = rustc_hash::FxHasher::default(); + mapper.origin.map(f64::to_bits).hash(&mut hasher); + for row in mapper.inverse_basis { + row.map(f64::to_bits).hash(&mut hasher); + } + hasher.finish() +} + fn morton_axis(mut value: u32) -> u64 { value &= 0x3ff; let mut result = 0u64; @@ -1197,6 +1216,62 @@ fn material_has_textures( }) } +fn material_has_box_projection( + material: Option<&crate::scene::model::material_model::MeshMaterial>, +) -> bool { + material.is_some_and(|material| { + [ + &material.diffuse_map, + &material.specular_map, + &material.reflection_map, + &material.opacity_map, + &material.bump_map, + &material.refraction_map, + &material.normal_map, + ] + .into_iter() + .any(|map| map.image.is_some() && map.projection == 2) + }) +} + +fn triangle_mapping_normal(mesh: &MeshModel, triangle: &[u32]) -> [f32; 3] { + let vertex = |corner: usize| { + let index = triangle[corner] as usize; + ( + mesh.verts[index], + mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]), + ) + }; + let (a, a_low) = vertex(0); + let (b, b_low) = vertex(1); + let (c, c_low) = vertex(2); + let ab = [ + (b[0] - a[0]) + (b_low[0] - a_low[0]), + (b[1] - a[1]) + (b_low[1] - a_low[1]), + (b[2] - a[2]) + (b_low[2] - a_low[2]), + ]; + let ac = [ + (c[0] - a[0]) + (c_low[0] - a_low[0]), + (c[1] - a[1]) + (c_low[1] - a_low[1]), + (c[2] - a[2]) + (c_low[2] - a_low[2]), + ]; + let normal = [ + ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0], + ]; + let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]) + .sqrt(); + if length > f32::EPSILON { + [normal[0] / length, normal[1] / length, normal[2] / length] + } else { + mesh.normals + .get(triangle[0] as usize) + .copied() + .unwrap_or([0.0, 1.0, 0.0]) + } +} + fn build_instanced_chunks( device: &wgpu::Device, queue: &wgpu::Queue, @@ -1215,9 +1290,10 @@ fn build_instanced_chunks( let compact_vertices = !material_has_textures(material); let material_identity = if material_has_textures(material) { material.map_or(0, |material| { - material + let handle = material .handle - .map_or(material as *const _ as usize as u64, |handle| handle.value()) + .map_or(material as *const _ as usize as u64, |handle| handle.value()); + handle ^ material_mapper_key(material).rotate_left(17) }) } else { 0 @@ -1229,7 +1305,8 @@ fn build_instanced_chunks( }; let budget = super::gpu_budget::buffer_budget(device); let needs_wire_vertices = first.include_edges && source.edge_verts.is_empty(); - let split_geometry = mesh + let split_geometry = (first.include_faces && material_has_box_projection(material)) + || mesh .verts .len() .saturating_mul(std::mem::size_of::()) @@ -1257,21 +1334,24 @@ fn build_instanced_chunks( let has_normals = mesh.normals.len() == mesh.verts.len(); let bounds = mesh_bounds(mesh); - let vertex = |index: usize| { + let vertex = |index: usize, mapping_normal: Option<[f32; 3]>| { let normal = if has_normals { mesh.normals[index] } else { [0.0, 1.0, 0.0] }; + let mapping_normal = mapping_normal.unwrap_or(normal); let position = mesh.verts[index]; let position_low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]); let uvs = material_uvs( material, position, - normal, + position_low, + mapping_normal, bounds, position, - normal, + position_low, + mapping_normal, bounds, ); MeshVertex { @@ -1289,7 +1369,7 @@ fn build_instanced_chunks( }; let verts: Vec<_> = if !split_geometry && (shared_vertex_buffer.is_none() || needs_wire_vertices) { - (0..mesh.verts.len()).map(vertex).collect() + (0..mesh.verts.len()).map(|index| vertex(index, None)).collect() } else { Vec::new() }; @@ -1398,10 +1478,16 @@ fn build_instanced_chunks( .max(1); if first.include_faces || needs_wire_vertices { for triangles in first.indices.chunks(max_triangles * 3) { - let vertices: Vec<_> = triangles - .iter() - .map(|index| vertex(*index as usize)) - .collect(); + let mut vertices = Vec::with_capacity(triangles.len()); + for triangle in triangles.chunks_exact(3) { + let mapping_normal = material_has_box_projection(material) + .then(|| triangle_mapping_normal(mesh, triangle)); + vertices.extend( + triangle + .iter() + .map(|index| vertex(*index as usize, mapping_normal)), + ); + } let indices: Vec<_> = (0..vertices.len() as u32).collect(); let wire_indices: Vec<_> = if needs_wire_vertices { indices @@ -1592,24 +1678,55 @@ fn material_is_transparent( || material.is_some_and(|material| { material.translucence > 0.0 || (material.channel_flags as u32 & 0x08 != 0 - && material.opacity_map.image.is_some()) + && material + .opacity_map + .image + .as_deref() + .is_some_and(|image| !opacity_image_is_binary_cutout(image))) }) } +fn opacity_image_is_binary_cutout( + image: &crate::scene::model::material_model::MaterialImage, +) -> bool { + let mut pixels = image.rgba.chunks_exact(4); + !image.rgba.is_empty() + && pixels.all(|pixel| matches!(pixel[0], 0 | 255)) + && pixels.remainder().is_empty() +} + fn material_map_uv( map: &crate::scene::model::material_model::MeshTextureMap, + mapper: Option<&crate::scene::model::material_model::MeshMaterialMapper>, local_position: [f32; 3], + local_position_low: [f32; 3], local_normal: [f32; 3], local_bounds: [f32; 6], model_position: [f32; 3], + model_position_low: [f32; 3], model_normal: [f32; 3], model_bounds: [f32; 6], ) -> [f32; 2] { - let (mut position, normal, bounds) = if map.auto_transform & 4 != 0 { - (model_position, model_normal, model_bounds) + let (mut position, position_low, mut normal, mut bounds) = if map.auto_transform & 4 != 0 { + ( + model_position, + model_position_low, + model_normal, + model_bounds, + ) } else { - (local_position, local_normal, local_bounds) + ( + local_position, + local_position_low, + local_normal, + local_bounds, + ) }; + if let Some(mapper) = mapper { + position = mapper.map_position(position, position_low); + normal = mapper.map_normal(normal); + bounds = mapper.map_bounds(bounds); + } if map.auto_transform & 2 != 0 { for axis in 0..3 { let extent = bounds[axis + 3] - bounds[axis]; @@ -1659,9 +1776,11 @@ fn material_map_uv( fn material_uvs( material: Option<&crate::scene::model::material_model::MeshMaterial>, local_position: [f32; 3], + local_position_low: [f32; 3], local_normal: [f32; 3], local_bounds: [f32; 6], model_position: [f32; 3], + model_position_low: [f32; 3], model_normal: [f32; 3], model_bounds: [f32; 6], ) -> [[f32; 2]; 7] { @@ -1674,10 +1793,13 @@ fn material_uvs( } else { material_map_uv( map, + material.mapper.as_ref(), local_position, + local_position_low, local_normal, local_bounds, model_position, + model_position_low, model_normal, model_bounds, ) @@ -2068,7 +2190,9 @@ pub fn build_mesh_batch_filtered( .visual_style .as_ref() .map_or(part.display_color, |style| style.edge_color(part.display_color)); - let vtx = |vi: usize| { + let vtx = |vi: usize, + local_mapping_normal: Option<[f32; 3]>, + model_mapping_normal: Option<[f32; 3]>| { let normal = if has_normals { mesh.normals[vi] } else { @@ -2079,6 +2203,11 @@ pub fn build_mesh_batch_filtered( .get(vi) .copied() .unwrap_or(mesh.verts[vi]); + let local_position_low = uv_mesh + .verts_low + .get(vi) + .copied() + .unwrap_or_else(|| mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3])); let local_normal = uv_mesh .normals .get(vi) @@ -2087,10 +2216,12 @@ pub fn build_mesh_batch_filtered( let uv = material_uvs( material, local_position, - local_normal, + local_position_low, + local_mapping_normal.unwrap_or(local_normal), local_bounds, mesh.verts[vi], - normal, + mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]), + model_mapping_normal.unwrap_or(normal), model_bounds, ); MeshVertex { @@ -2206,7 +2337,8 @@ pub fn build_mesh_batch_filtered( // A single mesh larger than a whole chunk: emit as triangle-soup // sub-chunks (corners expanded, no vertex sharing) so each buffer fits. - if mesh.verts.len() > max_verts || mesh_tris > max_tris { + let box_projected = part.include_faces && material_has_box_projection(material); + if mesh.verts.len() > max_verts || mesh_tris > max_tris || box_projected { if !verts.is_empty() || !edge_verts.is_empty() { chunks.push(make_chunk( device, @@ -2244,10 +2376,26 @@ pub fn build_mesh_batch_filtered( let (mut sv, mut si, mut swi) = (Vec::new(), Vec::new(), Vec::new()); for tri in t..end { let ix = &part.indices[tri * 3..tri * 3 + 3]; + let local_mapping_normal = + box_projected.then(|| triangle_mapping_normal(uv_mesh, ix)); + let model_mapping_normal = + box_projected.then(|| triangle_mapping_normal(mesh, ix)); let b = sv.len() as u32; - sv.push(vtx(ix[0] as usize)); - sv.push(vtx(ix[1] as usize)); - sv.push(vtx(ix[2] as usize)); + sv.push(vtx( + ix[0] as usize, + local_mapping_normal, + model_mapping_normal, + )); + sv.push(vtx( + ix[1] as usize, + local_mapping_normal, + model_mapping_normal, + )); + sv.push(vtx( + ix[2] as usize, + local_mapping_normal, + model_mapping_normal, + )); if part.include_faces { si.extend_from_slice(&[b, b + 1, b + 2]); } @@ -2359,7 +2507,7 @@ pub fn build_mesh_batch_filtered( } let base = verts.len() as u32; for i in 0..mesh.verts.len() { - verts.push(vtx(i)); + verts.push(vtx(i, None, None)); } if part.include_faces { let fill = if is_transp { &mut transp_indices } else { &mut indices }; @@ -2459,7 +2607,69 @@ pub fn build_mesh_batch_filtered( #[cfg(test)] mod texture_limit_tests { - use super::downscale_rgba_to_limit; + use super::{downscale_rgba_to_limit, opacity_image_is_binary_cutout}; + use crate::scene::model::material_model::MaterialImage; + use std::sync::Arc; + + #[test] + fn binary_opacity_image_uses_cutout_rendering() { + let image = MaterialImage { + width: 2, + height: 1, + rgba: Arc::new(vec![0, 0, 0, 255, 255, 255, 255, 255]), + path: String::new(), + }; + assert!(opacity_image_is_binary_cutout(&image)); + + let blended = MaterialImage { + rgba: Arc::new(vec![127, 127, 127, 255]), + width: 1, + height: 1, + path: String::new(), + }; + assert!(!opacity_image_is_binary_cutout(&blended)); + } + + #[test] + fn object_mapped_uv_is_stable_at_large_coordinates() { + let map = crate::scene::model::material_model::MeshTextureMap::default(); + let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + let local_mapper = crate::scene::model::material_model::MeshMaterialMapper { + origin: [1.0, 2.0, 3.0], + inverse_basis: identity, + normal_basis: identity, + }; + let local = super::material_map_uv( + &map, + Some(&local_mapper), + [3.0, 4.0, 5.0], + [0.0; 3], + [0.0, 0.0, 1.0], + [1.0, 2.0, 3.0, 11.0, 12.0, 13.0], + [3.0, 4.0, 5.0], + [0.0; 3], + [0.0, 0.0, 1.0], + [1.0, 2.0, 3.0, 11.0, 12.0, 13.0], + ); + let shifted_mapper = crate::scene::model::material_model::MeshMaterialMapper { + origin: [1_200_001.0, -799_998.0, 3.0], + inverse_basis: identity, + normal_basis: identity, + }; + let shifted = super::material_map_uv( + &map, + Some(&shifted_mapper), + [1_200_003.0, -799_996.0, 5.0], + [0.0; 3], + [0.0, 0.0, 1.0], + [1_200_001.0, -799_998.0, 3.0, 1_200_011.0, -799_988.0, 13.0], + [1_200_003.0, -799_996.0, 5.0], + [0.0; 3], + [0.0, 0.0, 1.0], + [1_200_001.0, -799_998.0, 3.0, 1_200_011.0, -799_988.0, 13.0], + ); + assert_eq!(local, shifted); + } #[test] fn downscale_keeps_both_dimensions_within_the_limit() { diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index 806abbe62..91751a8aa 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -583,6 +583,9 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { } material_alpha *= 1.0 - clamp(surface.ambient.a * max(surface.advanced.w, 0.0), 0.0, 0.95); material_alpha *= clamp(u.visual_style.y, 0.0, 1.0); + if (u.transparency_enable > 0.5 && material_alpha <= 0.001) { + discard; + } let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5); return vec4(rgb, alpha); }