use super::*;
const AUX_EDGE_WIDTH_PX: f32 = 1.0;
impl RenderCore {
fn face_base_color(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
let mut color = Self::face_base_color_opaque(solid, settings);
if let Some(opacity) = solid.opacity {
color[3] = opacity.clamp(0.0, 1.0);
}
color
}
fn face_base_color_opaque(solid: &SolidDisplay, settings: &RenderSettings) -> Rgba {
if let Some(rgb) = solid.color_override {
return Self::opaque_linear(rgb);
}
match settings.face_color_mode {
FaceColorMode::HashedBySolid => {
let srgb = solid_color_srgb(&solid.name);
[
srgb_to_linear(srgb[0]) as f32,
srgb_to_linear(srgb[1]) as f32,
srgb_to_linear(srgb[2]) as f32,
1.0,
]
}
FaceColorMode::Uniform => {
let c = settings.face_color;
[
srgb_to_linear(c[0] as f64) as f32,
srgb_to_linear(c[1] as f64) as f32,
srgb_to_linear(c[2] as f64) as f32,
c[3],
]
}
}
}
fn opaque_linear(rgb: [f32; 3]) -> Rgba {
[
srgb_to_linear(rgb[0] as f64) as f32,
srgb_to_linear(rgb[1] as f64) as f32,
srgb_to_linear(rgb[2] as f64) as f32,
1.0,
]
}
fn style_params(settings: &RenderSettings) -> [f32; 4] {
[if settings.flat_shading { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0]
}
fn face_style_palette(
&self,
solid: &SolidDisplay,
settings: &RenderSettings,
) -> (Vec<StyleBuf>, Vec<Option<u32>>) {
let mut styles: Vec<StyleBuf> = Vec::new();
let mut colors: Vec<[f32; 3]> = Vec::new();
let mut per_face: Vec<Option<u32>> = Vec::with_capacity(solid.faces.len());
let params = Self::style_params(settings);
for face in &solid.faces {
let Some(rgb) = face.color_override else {
per_face.push(None);
continue;
};
let slot = match colors.iter().position(|c| *c == rgb) {
Some(slot) => slot,
None => {
let style = StyleBuf::new(
&self.device,
&self.style_layout,
solid.name.as_str(),
);
style.write(&self.queue, Self::opaque_linear(rgb), params);
styles.push(style);
colors.push(rgb);
colors.len() - 1
}
};
per_face.push(Some(slot as u32));
}
(styles, per_face)
}
fn mesh_vertices(solid: &SolidDisplay) -> Vec<MeshVertex> {
solid
.mesh
.positions
.iter()
.zip(&solid.mesh.normals)
.map(|(&position, &normal)| MeshVertex { position, normal })
.collect()
}
fn edge_segments(solid: &SolidDisplay) -> (Vec<EdgeInstance>, Vec<EdgeRange>) {
let mut segments: Vec<EdgeInstance> = Vec::new();
let mut edges = Vec::with_capacity(solid.edges.len());
for edge in &solid.edges {
let first_instance = segments.len() as u32;
for pair in edge.polyline.windows(2) {
segments.push(EdgeInstance {
p0: pair[0],
p1: pair[1],
});
}
edges.push(EdgeRange {
first_instance,
instance_count: segments.len() as u32 - first_instance,
});
}
(segments, edges)
}
fn point_instances(solid: &SolidDisplay) -> Vec<PointInstance> {
solid
.vertices
.iter()
.map(|v| PointInstance {
center: [
v.position[0] as f32,
v.position[1] as f32,
v.position[2] as f32,
],
})
.collect()
}
fn repose_solid(&self, existing: &GpuSolid, solid: &SolidDisplay) -> bool {
let vertices = Self::mesh_vertices(solid);
let (segments, edges) = Self::edge_segments(solid);
let points = Self::point_instances(solid);
let same = vertices.len() as u32 == existing.vertex_count
&& segments.len() as u32 == existing.edge_instances
&& edges.len() == existing.edges.len()
&& points.len() as u32 == existing.point_count
&& solid.faces.len() == existing.faces.len()
&& existing.edge_buf.is_some() == !segments.is_empty()
&& existing.point_buf.is_some() == !points.is_empty();
if !same {
return false;
}
self.queue.write_buffer(&existing.vertex_buf, 0, bytemuck::cast_slice(&vertices));
if let Some(buf) = &existing.edge_buf {
self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&segments));
}
if let Some(buf) = &existing.point_buf {
self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&points));
}
true
}
fn upload_solid(&self, solid: &SolidDisplay, settings: &RenderSettings) -> GpuSolid {
let vertices = Self::mesh_vertices(solid);
let vertex_buf = self
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(solid.name.as_str()),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let index_buf = self
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(solid.name.as_str()),
contents: bytemuck::cast_slice(&solid.mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let tri_total = solid.mesh.indices.len() / 3;
let mut wire_indices: Vec<u32> = Vec::with_capacity(solid.mesh.indices.len());
let mut wire_ranges: Vec<(u32, u32)> = Vec::with_capacity(solid.faces.len());
let mut covered = vec![false; tri_total];
let mut seen: HashSet<(u32, u32), BuildEdgeHasher> = HashSet::default();
let mut emit_triangle = |wire: &mut Vec<u32>,
seen: &mut HashSet<(u32, u32), BuildEdgeHasher>,
tri: usize| {
let idx = [
solid.mesh.indices[tri * 3],
solid.mesh.indices[tri * 3 + 1],
solid.mesh.indices[tri * 3 + 2],
];
for k in 0..3 {
let (a, b) = (idx[k], idx[(k + 1) % 3]);
let key = if a < b { (a, b) } else { (b, a) };
if seen.insert(key) {
wire.push(a);
wire.push(b);
}
}
};
for face in &solid.faces {
let first = wire_indices.len() as u32;
seen.clear();
let start = (face.tri_start as usize).min(tri_total);
let end = (start + face.tri_count as usize).min(tri_total);
for tri in start..end {
covered[tri] = true;
emit_triangle(&mut wire_indices, &mut seen, tri);
}
wire_ranges.push((first, wire_indices.len() as u32 - first));
}
seen.clear();
for tri in 0..tri_total {
if !covered[tri] {
emit_triangle(&mut wire_indices, &mut seen, tri);
}
}
let wire_index_count = wire_indices.len() as u32;
let wire_index_buf = self
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(solid.name.as_str()),
contents: bytemuck::cast_slice(&wire_indices),
usage: wgpu::BufferUsages::INDEX,
});
let (face_styles, face_style_index) = self.face_style_palette(solid, settings);
let faces = solid
.faces
.iter()
.zip(&face_style_index)
.zip(&wire_ranges)
.map(|((face, style), &(wire_first, wire_count))| FaceRange {
first_index: face.tri_start * 3,
index_count: face.tri_count * 3,
wire_first,
wire_count,
style: *style,
})
.collect();
let (segments, edges) = Self::edge_segments(solid);
let occludable_edge_instances = solid
.edges
.iter()
.zip(&edges)
.find(|(edge, _)| edge.aux)
.map_or(segments.len() as u32, |(_, range): (_, &EdgeRange)| range.first_instance);
let edge_buf = (!segments.is_empty()).then(|| {
self.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(solid.name.as_str()),
contents: bytemuck::cast_slice(&segments),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
})
});
let points = Self::point_instances(solid);
let point_buf = (!points.is_empty()).then(|| {
self.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(solid.name.as_str()),
contents: bytemuck::cast_slice(&points),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
})
});
let base_style = StyleBuf::new(&self.device, &self.style_layout, solid.name.as_str());
base_style.write(
&self.queue,
Self::face_base_color(solid, settings),
Self::style_params(settings),
);
GpuSolid {
revision: solid.revision,
shape: Self::shape_of(solid),
vertex_count: vertices.len() as u32,
face_styles,
vertex_buf,
index_buf,
wire_index_buf,
wire_index_count,
faces,
edge_buf,
edges,
edge_instances: segments.len() as u32,
occludable_edge_instances,
point_buf,
point_count: points.len() as u32,
base_style,
boundary: None,
model_slot: 0,
holds_shape: solid.shape_revision == 0,
}
}
pub fn sync_scene(
&self,
gpu: &mut GpuScene,
scene: &RenderScene,
settings: &RenderSettings,
settings_generation: u64,
) {
gpu.order.clear();
let models = gpu.models.get_or_insert_with(|| ModelArena::new(&self.device, &self.model_layout, scene.solids().len() as u32));
let mut seen: Vec<&str> = Vec::with_capacity(scene.solids().len());
for solid in scene.solids() {
gpu.order.push(solid.name.clone());
seen.push(solid.name.as_str());
let refresh_style = gpu.settings_generation != settings_generation;
match gpu.solids.get_mut(&solid.name) {
Some(existing) if existing.revision == solid.revision => {
if refresh_style {
self.restyle(existing, solid, settings);
}
}
Some(existing)
if existing.holds_shape
&& existing.shape == Self::shape_of(solid)
&& (solid.pose.is_some() || solid.shape_revision == 0) =>
{
models.set(existing.model_slot, solid.pose.as_ref());
existing.revision = solid.revision;
gpu.pose_writes += 1;
if refresh_style {
self.restyle(existing, solid, settings);
}
}
Some(existing) if existing.shape == Self::shape_of(solid) && self.repose_solid(existing, solid) => {
models.set(existing.model_slot, None);
existing.holds_shape = false;
existing.revision = solid.revision;
gpu.reposes += 1;
if refresh_style {
self.restyle(existing, solid, settings);
}
}
_ => {
let mut uploaded = self.upload_solid(solid, settings);
match gpu.solids.get(&solid.name) {
Some(replaced) => uploaded.model_slot = replaced.model_slot,
None => uploaded.model_slot = models.alloc(),
}
models.set(uploaded.model_slot, None);
gpu.solids.insert(solid.name.clone(), uploaded);
gpu.uploads += 1;
}
}
}
let seen: std::collections::HashSet<&str> = seen.into_iter().collect();
gpu.solids.retain(|name, solid| {
let keep = seen.contains(name.as_str());
if !keep {
models.release(solid.model_slot);
}
keep
});
models.flush(&self.device, &self.queue, &self.model_layout);
gpu.settings_generation = settings_generation;
}
fn shape_of(solid: &SolidDisplay) -> u64 {
if solid.shape_revision != 0 {
solid.shape_revision
} else {
solid.revision
}
}
fn restyle(&self, existing: &GpuSolid, solid: &SolidDisplay, settings: &RenderSettings) {
let params = Self::style_params(settings);
existing.base_style.write(&self.queue, Self::face_base_color(solid, settings), params);
let mut seen: Vec<[f32; 3]> = Vec::new();
for face in &solid.faces {
let Some(rgb) = face.color_override else { continue };
if seen.contains(&rgb) {
continue;
}
if let Some(style) = existing.face_styles.get(seen.len()) {
style.write(&self.queue, Self::opaque_linear(rgb), params);
}
seen.push(rgb);
}
}
pub fn upload_scene(&self, scene: &RenderScene) -> GpuScene {
self.upload_scene_with(scene, &RenderSettings::artifact())
}
pub fn upload_scene_with(&self, scene: &RenderScene, settings: &RenderSettings) -> GpuScene {
let mut gpu = GpuScene::default();
self.sync_scene(&mut gpu, scene, settings, 0);
gpu
}
pub(super) fn ensure_targets(&mut self, width: u32, height: u32) {
if let Some(targets) = &self.targets {
if targets.width == width && targets.height == height {
return;
}
}
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let msaa = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("msaa color"),
size,
mip_level_count: 1,
sample_count: SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: self.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let depth = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth"),
size,
mip_level_count: 1,
sample_count: SAMPLES,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
self.targets = Some(CachedTargets {
width,
height,
msaa_view: msaa.create_view(&Default::default()),
depth_view: depth.create_view(&Default::default()),
});
}
pub(super) fn write_global_styles(&self, settings: &RenderSettings) {
let flat = if settings.flat_shading { 1.0 } else { 0.0 };
let face = |c: Rgba| -> Rgba {
[
srgb_to_linear(c[0] as f64) as f32,
srgb_to_linear(c[1] as f64) as f32,
srgb_to_linear(c[2] as f64) as f32,
c[3],
]
};
let s = &self.styles;
let q = &self.queue;
s.face_selected
.write(q, face(settings.face_selected_color), [flat, 0.0, 0.0, 0.0]);
s.face_hovered
.write(q, face(settings.hover_color), [flat, 0.0, 0.0, 0.0]);
let translucent = |rgba: [f32; 4], alpha: f32| [rgba[0], rgba[1], rgba[2], alpha];
self.context_selected.write(q, translucent(settings.face_selected_color, 0.6), [0.0; 4]);
self.context_hovered.write(q, translucent(settings.hover_color, 0.45), [0.0; 4]);
let edge_params = [settings.edge_width_px, EDGE_NUDGE, 0.0, 0.0];
s.edge_base.write(q, settings.edge_color, edge_params);
s.edge_selected.write(q, settings.edge_selected_color, edge_params);
s.edge_hovered.write(q, settings.hover_color, edge_params);
let mut hidden = settings.edge_color;
hidden[3] = settings.hidden_edge_alpha;
s.edge_hidden.write(q, hidden, edge_params);
s.edge_aux.write(q, settings.edge_color, [AUX_EDGE_WIDTH_PX, EDGE_NUDGE, 0.0, 0.0]);
s.boundary.write(
q,
settings.edge_selected_color,
[settings.edge_width_px + 1.0, EDGE_NUDGE * 1.5, 0.0, 0.0],
);
let point_params = [settings.vertex_size_px, 0.0, 0.0, 0.0];
s.point_base.write(q, settings.vertex_color, point_params);
s.point_selected
.write(q, settings.vertex_selected_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
s.point_hovered
.write(q, settings.hover_color, [settings.vertex_size_px + 1.0, 0.0, 0.0, 0.0]);
let axis_params = [2.0, EDGE_NUDGE, 0.0, 0.0];
s.axis_x.write(q, [0.91, 0.30, 0.32, 1.0], axis_params);
s.axis_y.write(q, [0.27, 0.80, 0.42, 1.0], axis_params);
s.axis_z.write(q, [0.23, 0.51, 0.96, 1.0], axis_params);
s.overlay_line.write(q, [1.0, 1.0, 1.0, 1.0], [2.5, 0.0, 0.0, 0.0]);
}
pub(super) fn sync_boundary(&self, gpu_solid: &mut GpuSolid, solid: &SolidDisplay, emphasis: &Emphasis) {
if let Some(boundary) = &gpu_solid.boundary {
if boundary.emphasis_generation == emphasis.generation
&& boundary.revision == solid.revision
{
return;
}
}
let mut segments: Vec<EdgeInstance> = Vec::new();
for face in &solid.faces {
if face.tri_count == 0 {
continue;
}
let state = emphasis.face_state(&solid.name, &face.name);
if state == EmphasisState::Base {
continue;
}
let mut edge_use: HashMap<(u32, u32), u32> = HashMap::new();
let start = face.tri_start as usize;
let end = (start + face.tri_count as usize).min(solid.mesh.indices.len() / 3);
for tri in start..end {
let idx = [
solid.mesh.indices[tri * 3],
solid.mesh.indices[tri * 3 + 1],
solid.mesh.indices[tri * 3 + 2],
];
for k in 0..3 {
let a = idx[k];
let b = idx[(k + 1) % 3];
let key = if a < b { (a, b) } else { (b, a) };
*edge_use.entry(key).or_insert(0) += 1;
}
}
let mut boundary: Vec<(u32, u32)> = edge_use
.into_iter()
.filter_map(|(key, count)| (count == 1).then_some(key))
.collect();
boundary.sort_unstable();
for (a, b) in boundary {
segments.push(EdgeInstance {
p0: solid.mesh.positions[a as usize],
p1: solid.mesh.positions[b as usize],
});
}
}
let buf = (!segments.is_empty()).then(|| {
self.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("emphasis boundary"),
contents: bytemuck::cast_slice(&segments),
usage: wgpu::BufferUsages::VERTEX,
})
});
gpu_solid.boundary = Some(BoundaryBuf {
emphasis_generation: emphasis.generation,
revision: solid.revision,
buf,
count: segments.len() as u32,
});
}
}