feat(scene): integrate standard DWG data

This commit is contained in:
Hakan Seven 2026-07-26 22:30:14 +03:00
commit eb23715605
38 changed files with 8365 additions and 607 deletions

View file

@ -20,6 +20,23 @@ pub struct MeshVertex {
pub normal: [f32; 3],
pub color: [f32; 4],
pub position_low: [f32; 3],
/// gloss, reflectivity, self-illumination, luminance
pub material: [f32; 4],
/// specular RGB and refraction index
pub specular: [f32; 4],
pub uv_diffuse: [f32; 2],
/// ambient RGB and translucence
pub ambient: [f32; 4],
/// normal strength, bump scale, reflectance scale, transmittance scale
pub advanced: [f32; 4],
/// illumination model, channel flags, material mode, luminance mode
pub flags: [u32; 4],
pub uv_specular: [f32; 2],
pub uv_reflection: [f32; 2],
pub uv_opacity: [f32; 2],
pub uv_bump: [f32; 2],
pub uv_refraction: [f32; 2],
pub uv_normal: [f32; 2],
}
impl MeshVertex {
@ -45,6 +62,66 @@ impl MeshVertex {
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, material) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, specular) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_diffuse) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, ambient) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, advanced) as u64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, flags) as u64,
shader_location: 9,
format: wgpu::VertexFormat::Uint32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_specular) as u64,
shader_location: 10,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_reflection) as u64,
shader_location: 11,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_opacity) as u64,
shader_location: 12,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_bump) as u64,
shader_location: 13,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_refraction) as u64,
shader_location: 14,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_normal) as u64,
shader_location: 15,
format: wgpu::VertexFormat::Float32x2,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshVertex>() as u64,
@ -128,6 +205,8 @@ pub struct MeshBatchChunk {
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
pub edge_vertex_buffer: wgpu::Buffer,
pub edge_vertex_count: u32,
pub material: Option<crate::scene::model::material_model::MeshMaterial>,
pub material_bind_group: Option<wgpu::BindGroup>,
}
fn make_chunk(
@ -137,6 +216,7 @@ fn make_chunk(
transp_indices: &[u32],
wire_indices: &[u32],
edge_verts: &[MeshVertex],
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> MeshBatchChunk {
// `create_buffer_init` with an empty slice yields a zero-sized buffer that
// some backends reject for INDEX usage; a chunk can legitimately hold only
@ -157,6 +237,18 @@ fn make_chunk(
normal: [0.0, 1.0, 0.0],
color: [0.0; 4],
position_low: [0.0; 3],
material: [0.0; 4],
specular: [1.0, 1.0, 1.0, 1.0],
uv_diffuse: [0.0; 2],
ambient: [0.3, 0.3, 0.3, 0.0],
advanced: [1.0; 4],
flags: [0, 127, 0, 0],
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
}];
let src = if data.is_empty() { &stub[..] } else { data };
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -175,9 +267,411 @@ fn make_chunk(
wire_index_count: wire_indices.len() as u32,
edge_vertex_buffer: mk_vertex(edge_verts, "mesh.batch.edge_vbuf"),
edge_vertex_count: edge_verts.len() as u32,
material: material.cloned(),
material_bind_group: None,
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct MaterialMapParams {
/// diffuse, specular, reflection and opacity blend factors.
blends0: [f32; 4],
/// Presence bits for the same four maps.
present0: [u32; 4],
/// bump, refraction, normal and reserved blend factors.
blends1: [f32; 4],
/// Presence bits for the same four maps.
present1: [u32; 4],
}
fn upload_rgba_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &'static str,
image: Option<&crate::scene::model::material_model::MaterialImage>,
fallback: [u8; 4],
srgb: bool,
) -> wgpu::TextureView {
let (width, height, pixels) = image.map_or((1, 1, fallback.as_slice()), |image| {
(image.width, image.height, image.rgba.as_slice())
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: if srgb {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
pub fn create_material_bind_group(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> wgpu::BindGroup {
let diffuse = material.and_then(|material| material.diffuse_map.image.as_deref());
let specular = material.and_then(|material| material.specular_map.image.as_deref());
let reflection = material.and_then(|material| material.reflection_map.image.as_deref());
let opacity = material.and_then(|material| material.opacity_map.image.as_deref());
let bump = material.and_then(|material| material.bump_map.image.as_deref());
let refraction = material.and_then(|material| material.refraction_map.image.as_deref());
let normal = material.and_then(|material| material.normal_map.image.as_deref());
let diffuse_view =
upload_rgba_texture(device, queue, "mesh.material.diffuse", diffuse, [255; 4], true);
let specular_view =
upload_rgba_texture(device, queue, "mesh.material.specular", specular, [255; 4], true);
let reflection_view =
upload_rgba_texture(device, queue, "mesh.material.reflection", reflection, [0, 0, 0, 255], true);
let opacity_view =
upload_rgba_texture(device, queue, "mesh.material.opacity", opacity, [255; 4], false);
let bump_view =
upload_rgba_texture(device, queue, "mesh.material.bump", bump, [128, 128, 128, 255], false);
let refraction_view =
upload_rgba_texture(device, queue, "mesh.material.refraction", refraction, [255; 4], true);
let normal_view = upload_rgba_texture(
device,
queue,
"mesh.material.normal",
normal,
[128, 128, 255, 255],
false,
);
let sampler = |label: &'static str, tiling: u8| {
let address = match tiling {
1 => wgpu::AddressMode::Repeat,
4 => wgpu::AddressMode::MirrorRepeat,
_ => wgpu::AddressMode::ClampToEdge,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
address_mode_u: address,
address_mode_v: address,
address_mode_w: address,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
})
};
let tiling = |channel: usize| {
material.map_or(1, |material| {
[
material.diffuse_map.tiling,
material.specular_map.tiling,
material.reflection_map.tiling,
material.opacity_map.tiling,
material.bump_map.tiling,
material.refraction_map.tiling,
material.normal_map.tiling,
][channel]
})
};
let diffuse_sampler = sampler("mesh.material.diffuse_sampler", tiling(0));
let specular_sampler = sampler("mesh.material.specular_sampler", tiling(1));
let reflection_sampler = sampler("mesh.material.reflection_sampler", tiling(2));
let opacity_sampler = sampler("mesh.material.opacity_sampler", tiling(3));
let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4));
let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5));
let normal_sampler = sampler("mesh.material.normal_sampler", tiling(6));
let params = MaterialMapParams {
blends0: material.map_or([0.0; 4], |material| {
[
material.diffuse_map.blend_factor,
material.specular_map.blend_factor,
material.reflection_map.blend_factor,
material.opacity_map.blend_factor,
]
}),
present0: material.map_or([0; 4], |material| {
[
material.diffuse_map.image.is_some() as u32,
material.specular_map.image.is_some() as u32,
material.reflection_map.image.is_some() as u32,
material.opacity_map.image.is_some() as u32,
]
}),
blends1: material.map_or([0.0; 4], |material| {
[
material.bump_map.blend_factor,
material.refraction_map.blend_factor,
material.normal_map.blend_factor,
0.0,
]
}),
present1: material.map_or([0; 4], |material| {
[
material.bump_map.image.is_some() as u32,
material.refraction_map.image.is_some() as u32,
material.normal_map.image.is_some() as u32,
0,
]
}),
};
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.material.params"),
contents: bytemuck::bytes_of(&params),
usage: wgpu::BufferUsages::UNIFORM,
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mesh.material.bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&specular_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&reflection_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&opacity_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(&bump_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&refraction_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&normal_view),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::Sampler(&specular_sampler),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::Sampler(&reflection_sampler),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&opacity_sampler),
},
wgpu::BindGroupEntry {
binding: 11,
resource: wgpu::BindingResource::Sampler(&bump_sampler),
},
wgpu::BindGroupEntry {
binding: 12,
resource: wgpu::BindingResource::Sampler(&refraction_sampler),
},
wgpu::BindGroupEntry {
binding: 13,
resource: wgpu::BindingResource::Sampler(&normal_sampler),
},
wgpu::BindGroupEntry {
binding: 14,
resource: params_buffer.as_entire_binding(),
},
],
})
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct MaterialBatchKey([u32; 16]);
fn material_key(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> MaterialBatchKey {
let Some(material) = material else {
return MaterialBatchKey([
0,
0,
color[0].to_bits(),
color[1].to_bits(),
color[2].to_bits(),
color[3].to_bits(),
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]);
};
let handle = material.handle.map_or(0, |handle| handle.value());
MaterialBatchKey([
handle as u32,
(handle >> 32) as u32,
material.diffuse[0].to_bits(),
material.diffuse[1].to_bits(),
material.diffuse[2].to_bits(),
material.diffuse[3].to_bits(),
material.specular[0].to_bits(),
material.specular[1].to_bits(),
material.specular[2].to_bits(),
material.gloss.to_bits(),
material.reflectivity.to_bits(),
material.self_illumination.to_bits(),
material.luminance.to_bits(),
material.refraction_index.to_bits(),
material.diffuse_map.projection as u32,
material.diffuse_map.tiling as u32,
])
}
struct MeshBatchPart<'a> {
set: &'a MeshLodSet,
mesh: &'a MeshModel,
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: Vec<u32>,
include_faces: bool,
include_edges: bool,
}
fn material_map_uv(
map: &crate::scene::model::material_model::MeshTextureMap,
position: [f32; 3],
normal: [f32; 3],
) -> [f32; 2] {
let m = &map.transform;
let p = [
position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3],
position[0] * m[4] + position[1] * m[5] + position[2] * m[6] + m[7],
position[0] * m[8] + position[1] * m[9] + position[2] * m[10] + m[11],
];
match map.projection {
3 => [
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
p[2],
],
4 => {
let radius = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
if radius <= f32::EPSILON {
[0.0; 2]
} else {
[
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
(p[2] / radius).clamp(-1.0, 1.0).acos() / std::f32::consts::PI,
]
}
}
2 => {
let n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
if n[0] >= n[1] && n[0] >= n[2] {
[p[1], p[2]]
} else if n[1] >= n[2] {
[p[0], p[2]]
} else {
[p[0], p[1]]
}
}
_ => [p[0], p[1]],
}
}
fn material_uvs(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
position: [f32; 3],
normal: [f32; 3],
) -> [[f32; 2]; 7] {
let Some(material) = material else {
return [[0.0; 2]; 7];
};
[
material_map_uv(&material.diffuse_map, position, normal),
material_map_uv(&material.specular_map, position, normal),
material_map_uv(&material.reflection_map, position, normal),
material_map_uv(&material.opacity_map, position, normal),
material_map_uv(&material.bump_map, position, normal),
material_map_uv(&material.refraction_map, position, normal),
material_map_uv(&material.normal_map, position, normal),
]
}
fn material_vertex_params(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> ([f32; 4], [f32; 4], [f32; 4], [f32; 4], [u32; 4]) {
let Some(material) = material else {
return (
[0.5, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0],
[0.3, 0.3, 0.3, 0.0],
[1.0; 4],
[0, 127, 0, 0],
);
};
(
[
material.gloss,
material.reflectivity,
material.self_illumination,
material.luminance,
],
[
material.specular[0],
material.specular[1],
material.specular[2],
material.refraction_index,
],
[
material.ambient[0],
material.ambient[1],
material.ambient[2],
material.translucence,
],
[
material.normal_map_strength,
material.indirect_bump_scale,
material.reflectance_scale,
material.transmittance_scale,
],
[
material.illumination_model as u32,
material.channel_flags as u32,
material.mode as u32,
material.luminance_mode as u32,
],
)
}
/// Concatenate every set's first non-empty LOD into a few large GPU buffers.
/// Returns the chunks plus the total triangle count drawn (for diagnostics).
///
@ -202,34 +696,181 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
let mut wire_indices: Vec<u32> = Vec::new();
let mut edge_verts: Vec<MeshVertex> = Vec::new();
let mut total_tris = 0u64;
let mut ordered: Vec<MeshBatchPart<'_>> = Vec::new();
for set in sets {
let Some(mesh) = set.lods.iter().find(|m| !m.indices.is_empty()) else {
let Some(mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
continue;
};
let triangle_count = mesh.indices.len() / 3;
let has_face_materials = mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty();
let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& mesh.triangle_colors.iter().any(Option::is_some);
let include_faces = set
.visual_style
.as_ref()
.map_or(true, |style| style.face_visible());
let include_edges = set
.visual_style
.as_ref()
.map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors {
let base_color =
set.material.as_ref().map_or(mesh.color, |material| material.diffuse);
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
ordered.push(MeshBatchPart {
set,
mesh,
material: set.material.as_ref(),
color,
indices: mesh.indices.clone(),
include_faces,
include_edges,
});
continue;
}
let mut groups: std::collections::BTreeMap<
MaterialBatchKey,
(
Option<&crate::scene::model::material_model::MeshMaterial>,
[f32; 4],
Vec<u32>,
),
> = std::collections::BTreeMap::new();
for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() {
let material = if has_face_materials {
mesh.triangle_material_handles[triangle]
.and_then(|handle| set.face_materials.get(&handle))
.or(set.material.as_ref())
} else {
set.material.as_ref()
};
let base_color = material.map_or(mesh.color, |material| material.diffuse);
let base_color = if has_face_colors {
mesh.triangle_colors[triangle].unwrap_or(base_color)
} else {
base_color
};
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
groups
.entry(material_key(material, color))
.or_insert_with(|| (material, color, Vec::new()))
.2
.extend_from_slice(indices);
}
for (part_index, (_, (material, color, indices))) in groups.into_iter().enumerate() {
ordered.push(MeshBatchPart {
set,
mesh,
material,
color,
indices,
include_faces,
include_edges: include_edges && part_index == 0,
});
}
}
ordered.sort_by_key(|part| {
material_key(part.material, part.color)
});
let mut active_key: Option<MaterialBatchKey> = None;
let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None;
for part in ordered {
let set = part.set;
let mesh = part.mesh;
let material = part.material;
let part_color = part.color;
let key = material_key(material, part_color);
if active_key.is_some_and(|active| active != key)
&& (!verts.is_empty() || !edge_verts.is_empty())
{
chunks.push(make_chunk(
device,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
}
active_key = Some(key);
active_material = material;
let has_normals = mesh.normals.len() == mesh.verts.len();
let vtx = |vi: usize| MeshVertex {
position: mesh.verts[vi],
normal: if has_normals { mesh.normals[vi] } else { [0.0, 1.0, 0.0] },
color: mesh.color,
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let edge_color = set
.visual_style
.as_ref()
.map_or(mesh.color, |style| style.edge_color(mesh.color));
let vtx = |vi: usize| {
let normal = if has_normals {
mesh.normals[vi]
} else {
[0.0, 1.0, 0.0]
};
let uv = material_uvs(material, mesh.verts[vi], normal);
MeshVertex {
position: mesh.verts[vi],
normal,
color: part_color,
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: uv[0],
ambient,
advanced,
flags,
uv_specular: uv[1],
uv_reflection: uv[2],
uv_opacity: uv[3],
uv_bump: uv[4],
uv_refraction: uv[5],
uv_normal: uv[6],
}
};
// A solid whose baked colour is not fully opaque routes into the
// transparent index stream so it is drawn last, without depth writes.
let is_transp = mesh.color[3] < 0.999;
let mesh_tris = mesh.indices.len() / 3;
total_tris += mesh_tris as u64;
let is_transp = part_color[3] < 0.999;
let mesh_tris = part.indices.len() / 3;
if part.include_faces {
total_tris += mesh_tris as u64;
}
// Feature edges present (ACIS solid) → emit the B-rep edges as a line
// list and skip the triangulation wireframe. Absent (plain mesh) → keep
// the triangle edges so the mesh still shows a wireframe.
let has_feat = !set.edge_verts.is_empty();
if has_feat {
if has_feat && part.include_edges {
for i in 0..set.edge_verts.len() {
edge_verts.push(MeshVertex {
position: set.edge_verts[i],
normal: [0.0, 1.0, 0.0],
color: mesh.color,
color: edge_color,
position_low: set.edge_verts_low.get(i).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: [0.0; 2],
ambient,
advanced,
flags,
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
});
}
}
@ -245,6 +886,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
@ -258,22 +900,40 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
let end = (t + tris_per).min(mesh_tris);
let (mut sv, mut si, mut swi) = (Vec::new(), Vec::new(), Vec::new());
for tri in t..end {
let ix = &mesh.indices[tri * 3..tri * 3 + 3];
let ix = &part.indices[tri * 3..tri * 3 + 3];
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));
si.extend_from_slice(&[b, b + 1, b + 2]);
if !has_feat {
if part.include_faces {
si.extend_from_slice(&[b, b + 1, b + 2]);
}
if part.include_edges && !has_feat {
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
}
}
// The whole mesh shares one colour, so a sub-chunk is entirely
// opaque or entirely transparent.
if is_transp {
chunks.push(make_chunk(device, &sv, &[], &si, &swi, &[]));
chunks.push(make_chunk(
device,
&sv,
&[],
&si,
&swi,
&[],
active_material,
));
} else {
chunks.push(make_chunk(device, &sv, &si, &[], &swi, &[]));
chunks.push(make_chunk(
device,
&sv,
&si,
&[],
&swi,
&[],
active_material,
));
}
t = end;
}
@ -293,6 +953,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
@ -304,18 +965,24 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
for i in 0..mesh.verts.len() {
verts.push(vtx(i));
}
let fill = if is_transp { &mut transp_indices } else { &mut indices };
for &idx in &mesh.indices {
fill.push(base + idx);
if part.include_faces {
let fill = if is_transp { &mut transp_indices } else { &mut indices };
for &idx in &part.indices {
fill.push(base + idx);
}
}
if !has_feat {
for tri in mesh.indices.chunks_exact(3) {
if part.include_edges && !has_feat {
for tri in part.indices.chunks_exact(3) {
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
}
}
}
if !indices.is_empty() || !transp_indices.is_empty() || !edge_verts.is_empty() {
if !indices.is_empty()
|| !transp_indices.is_empty()
|| !wire_indices.is_empty()
|| !edge_verts.is_empty()
{
chunks.push(make_chunk(
device,
&verts,
@ -323,6 +990,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
}
(chunks, total_tris)
@ -336,7 +1004,7 @@ impl MeshLodGpu {
.lods
.iter()
.filter(|m| !m.indices.is_empty())
.map(|m| MeshGpu::new(device, m, highlight))
.map(|m| MeshGpu::new(device, m, set.material.as_ref(), highlight))
.collect(),
world_aabb: set.world_aabb,
}
@ -344,7 +1012,12 @@ impl MeshLodGpu {
}
impl MeshGpu {
pub fn new(device: &wgpu::Device, mesh: &MeshModel, highlight: Highlight) -> Self {
pub fn new(
device: &wgpu::Device,
mesh: &MeshModel,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
highlight: Highlight,
) -> Self {
let has_normals = mesh.normals.len() == mesh.verts.len();
// Blend the base colour toward the highlight so a selected / hovered
// solid reads clearly while keeping some shape shading.
@ -358,6 +1031,8 @@ impl MeshGpu {
}
None => mesh.color,
};
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let vertices: Vec<MeshVertex> = mesh
.verts
.iter()
@ -368,11 +1043,24 @@ impl MeshGpu {
} else {
[0.0, 1.0, 0.0]
};
let uv = material_uvs(material, pos, normal);
MeshVertex {
position: pos,
normal,
color,
position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: uv[0],
ambient,
advanced,
flags,
uv_specular: uv[1],
uv_reflection: uv[2],
uv_opacity: uv[3],
uv_bump: uv[4],
uv_refraction: uv[5],
uv_normal: uv[6],
}
})
.collect();

View file

@ -78,6 +78,8 @@ pub struct Pipeline {
/// writes, writes depth). Used in HiddenLine mode so 3D solids
/// occlude wires behind them without painting visible pixels.
mesh_depth_pipeline: wgpu::RenderPipeline,
mesh_material_bgl: wgpu::BindGroupLayout,
mesh_default_material_bind_group: wgpu::BindGroup,
face3d_pipeline: wgpu::RenderPipeline,
/// Depth-only variant of the face3d pipeline (no color writes,
/// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine.
@ -839,9 +841,144 @@ impl Pipeline {
))),
});
let mesh_material_bgl =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mesh.material.bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 11,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 12,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 13,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 14,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let mesh_default_material_bind_group = mesh_gpu::create_material_bind_group(
device,
queue,
&mesh_material_bgl,
None,
);
let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh.pipeline_layout"),
bind_group_layouts: &[&frame_bgl],
bind_group_layouts: &[&frame_bgl, &mesh_material_bgl],
push_constant_ranges: &[],
});
@ -1437,6 +1574,8 @@ impl Pipeline {
mesh_wireframe_pipeline,
mesh_edge_black_pipeline,
mesh_depth_pipeline,
mesh_material_bgl,
mesh_default_material_bind_group,
face3d_pipeline,
face3d_depth_pipeline,
uniform_buffer,
@ -1757,6 +1896,18 @@ impl Pipeline {
(w.y - hy as f64) as f32,
(w.z - hz as f64) as f32,
],
material: [0.5, 0.0, 0.0, 0.0],
specular: [1.0, 1.0, 1.0, 1.0],
uv_diffuse: [0.0; 2],
ambient: [0.3, 0.3, 0.3, 0.0],
advanced: [1.0; 4],
flags: [0, 127, 0, 0],
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
}
};
for g in &set.curved_gens {
@ -1863,6 +2014,28 @@ impl Pipeline {
}
}
}
if set.curved_gens.is_empty() || !set.complete {
let best = set.stored_silhouettes.iter().max_by(|left, right| {
let score = |silhouette: &crate::scene::model::mesh_model::StoredSilhouette| {
let direction = d3(silhouette.view_direction)
.normalize_or(glam::DVec3::NEG_Z);
direction.dot(view).abs()
};
score(left)
.partial_cmp(&score(right))
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(silhouette) = best {
for (index, high) in silhouette.edge_verts.iter().copied().enumerate() {
let low = silhouette
.edge_verts_low
.get(index)
.copied()
.unwrap_or([0.0; 3]);
verts.push(mk(lo(high, low)));
}
}
}
}
if verts.is_empty() {
self.silhouette_vbuf = None;
@ -2020,8 +2193,21 @@ impl Pipeline {
/// set, drawn in a handful of calls). Selection/hover tint is intentionally
/// not applied here — the batch is geometry-only and stays resident across
/// camera moves and pick changes.
pub fn upload_mesh_batch(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) {
let (chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes);
pub fn upload_mesh_batch(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
meshes: &[MeshLodSet],
) {
let (mut chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes);
for chunk in &mut chunks {
chunk.material_bind_group = Some(mesh_gpu::create_material_bind_group(
device,
queue,
&self.mesh_material_bgl,
chunk.material.as_ref(),
));
}
self.gpu_mesh_batch = chunks;
}
@ -2051,7 +2237,7 @@ impl Pipeline {
Some(h) if Some(h) == hover => Highlight::Hover,
_ => continue,
};
out.push(MeshGpu::new(device, m, mode));
out.push(MeshGpu::new(device, m, set.material.as_ref(), mode));
}
self.gpu_mesh_highlight = out;
}
@ -2377,6 +2563,13 @@ impl Pipeline {
// so both the opaque and the transparent tris write depth here.
pass.set_pipeline(&self.mesh_depth_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
if c.index_count != 0 {
pass.set_index_buffer(
@ -2395,6 +2588,13 @@ impl Pipeline {
}
pass.set_pipeline(&self.mesh_wireframe_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
// Plain-mesh triangulation edges.
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
@ -2419,6 +2619,13 @@ impl Pipeline {
if mesh_wireframe {
pass.set_pipeline(&self.mesh_wireframe_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_index_buffer(
@ -2441,6 +2648,13 @@ impl Pipeline {
// Opaque fills first (they write depth).
pass.set_pipeline(&self.mesh_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.index_count == 0 {
continue;
}
@ -2453,6 +2667,13 @@ impl Pipeline {
// culling it via the depth buffer.
pass.set_pipeline(&self.mesh_transparent_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.transp_index_count == 0 {
continue;
}
@ -2469,6 +2690,7 @@ impl Pipeline {
// the tint shows on top even when the solid is behind others.
if !self.gpu_mesh_highlight.is_empty() {
pass.set_pipeline(&self.mesh_highlight_pipeline);
pass.set_bind_group(1, &self.mesh_default_material_bind_group, &[]);
for g in &self.gpu_mesh_highlight {
if g.index_count == 0 {
continue;
@ -2484,6 +2706,13 @@ impl Pipeline {
if want_solid_with_edges {
pass.set_pipeline(&self.mesh_edge_black_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_index_buffer(