forked from 1b-agent/opencadstudio-upstream
Render procedural checker materials correctly
This commit is contained in:
1 parent
12c1c330fc
commit
07acd1284c
9 files changed
+440
-39
No files matched your search
+234
-24
@@ -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::<MeshVertex>())
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user