use core::mem::size_of;
use std::rc::Rc;
use arrayvec::ArrayVec;
use unlit_ecs::Scope;
use unlit_wgpu::mesh::{
ChannelEncoding, MeshInstance, MeshMetadata, PositionStreamChannels, UvColorFlags,
compress_weights, index_fits_u16,
};
use unlit_wgpu::pipeline::{
BASE_COLOR_SAMPLER_BINDING, BASE_COLOR_TEXTURE_BINDING, GlobalBindings, INSTANCE_SLOT,
POSITION_SLOT, UV_COLOR_SLOT, UnlitOptions, UnlitVariant, UnlitVertexChannels,
};
use unlit_wgpu::resources::{ResHandle, TextureView};
use unlit_wgpu::scene::MAX_VERTEX_BUFFERS;
use unlit_wgpu::specialize::{
SpecializedPipeline, SurfaceKey, VertexAttributes, VertexBufferLayoutDesc, VertexLayout,
};
use unlit_wgpu::util::Hashed;
use unlit_wgpu::vertex_pool::VertexStreamPool;
use zerocopy::IntoBytes;
use crate::bounds::Aabb;
use crate::components::{GpuMaterial, GpuMesh, GpuRenderPipeline};
use crate::mesh::{MeshDesc, MorphDeltas};
use crate::mesh_source::MeshSource;
use crate::pipeline::{
DrawContext, FamilyContext, InstanceContext, InstanceData, InstanceStreamDesc,
RegisteredRenderPipeline, RenderPipelineFactory, RenderPipelineKey,
};
fn index_format_size(format: wgpu::IndexFormat) -> u32 {
match format {
wgpu::IndexFormat::Uint16 => size_of::<u16>() as u32,
wgpu::IndexFormat::Uint32 => size_of::<u32>() as u32,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct IndexOffset {
baked: u32,
draw_base_vertex: u32,
}
impl IndexOffset {
fn new(supports_base_vertex: bool, vertex_offset: u32) -> Self {
if supports_base_vertex {
Self {
baked: 0,
draw_base_vertex: vertex_offset,
}
} else {
Self {
baked: vertex_offset,
draw_base_vertex: 0,
}
}
}
fn packed_len(self, indices: &[u32]) -> (wgpu::IndexFormat, usize) {
let widened = indices
.iter()
.any(|&index| !index_fits_u16(index + self.baked));
let element = if widened {
size_of::<u32>()
} else {
size_of::<u16>()
};
let format = if widened {
wgpu::IndexFormat::Uint32
} else {
wgpu::IndexFormat::Uint16
};
let padded_len =
(indices.len() * element).next_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT as usize);
(format, padded_len)
}
fn write(self, indices: &[u32], out: wgpu::WriteOnly<'_, [u8]>) {
let (format, padded_len) = self.packed_len(indices);
assert_eq!(
out.len(),
padded_len,
"the target must hold exactly the packed index buffer"
);
let element = index_format_size(format) as usize;
let (data, mut padding) = out.split_at(indices.len() * element);
match format {
wgpu::IndexFormat::Uint16 => {
let (chunks, _remainder) = data.into_chunks::<{ size_of::<u16>() }>();
chunks.write_iter(
indices
.iter()
.map(|&index| (index + self.baked) as u16)
.map(u16::to_ne_bytes),
);
}
wgpu::IndexFormat::Uint32 => {
let (chunks, _remainder) = data.into_chunks::<{ size_of::<u32>() }>();
chunks.write_iter(
indices
.iter()
.map(|&index| index + self.baked)
.map(u32::to_ne_bytes),
);
}
}
padding.fill(0);
}
#[cfg(test)]
fn pack(self, indices: &[u32]) -> (wgpu::IndexFormat, Vec<u8>) {
let (format, padded_len) = self.packed_len(indices);
let mut data = vec![0u8; padded_len];
self.write(indices, wgpu::WriteOnly::from_mut(data.as_mut_slice()));
(format, data)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct UnlitPipelineKey {
pub options: Hashed<UnlitOptions>,
}
impl UnlitPipelineKey {
pub fn new(options: UnlitOptions) -> Self {
Self {
options: Hashed::new(options),
}
}
}
impl RenderPipelineKey for UnlitPipelineKey {
type Variant = UnlitVariant;
fn variant(&self, draw: &DrawContext<'_>) -> UnlitVariant {
let index_format = draw
.mesh
.parts
.index_buffer
.as_ref()
.map(|(_, format)| *format);
UnlitVariant {
options: self.options.clone(),
surface: draw.surface,
channels: UnlitVertexChannels::for_vertex_layout(&draw.mesh.vertex_layout),
base_color_texture: draw.material.is_some(),
morph: draw.mesh.morph_targets > 0,
strip_index_format: strip_index_format(self.options.primitive.topology, index_format),
}
}
}
fn strip_index_format(
topology: wgpu::PrimitiveTopology,
index_format: Option<wgpu::IndexFormat>,
) -> Option<wgpu::IndexFormat> {
index_format.filter(|_| topology.is_strip())
}
struct UnlitFactory;
impl RenderPipelineFactory<UnlitVariant> for UnlitFactory {
fn descriptor(
&self,
context: &FamilyContext<'_>,
value: &SpecializedPipeline<wgpu::RenderPipeline, UnlitVariant>,
) -> RegisteredRenderPipeline {
let variant = value.descriptor();
let bindings = GlobalBindings {
metadata: variant.needs_metadata(),
joints: variant.needs_joints(),
morphs: variant.needs_morphs(),
};
let layout = context.resources.layout(context.device, bindings);
let rebuild = context.resources.rebuild(context.device, layout, bindings);
RegisteredRenderPipeline {
pipeline: value.pipeline.clone(),
global: Some(rebuild),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct UnlitMeshDesc<'a> {
pub positions: &'a [[f32; 3]],
pub uvs: Option<&'a [[f32; 2]]>,
pub colors: Option<&'a [[u8; 4]]>,
pub indices: Option<&'a [u32]>,
pub joints: Option<&'a [[u16; 4]]>,
pub weights: Option<&'a [[f32; 4]]>,
pub morph_deltas: Option<MorphDeltas>,
}
pub type UnlitPipeline = GpuRenderPipeline<UnlitPipelineKey>;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct InstanceColor {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Vec4Proxy))]
pub color: glam::Vec4,
}
impl Default for InstanceColor {
fn default() -> Self {
Self {
color: glam::Vec4::ONE,
}
}
}
impl InstanceColor {
pub const fn new(color: glam::Vec4) -> Self {
Self { color }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct InstanceCutoff {
pub cutoff: f32,
}
impl Default for InstanceCutoff {
fn default() -> Self {
Self { cutoff: 0.0 }
}
}
impl InstanceCutoff {
pub const fn new(cutoff: f32) -> Self {
Self { cutoff }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct UnlitInstance;
impl InstanceData for UnlitInstance {
fn stream(&self) -> InstanceStreamDesc {
InstanceStreamDesc {
slot: INSTANCE_SLOT,
array_stride: size_of::<MeshInstance>() as u32,
}
}
fn write(&mut self, context: &mut InstanceContext<'_>, out: &mut [u8]) {
let world = &context.world;
let entity = context.entity;
let mesh = world.query_one::<&GpuMesh>(entity).unwrap_or_else(|_| {
panic!(
"every visible entity carries the mesh culling resolved, but {entity:?} does not"
)
});
let base_color = world
.query_one::<&InstanceColor>(entity)
.map_or(glam::Vec4::ONE, |color| color.color);
let cutoff = world
.query_one::<&InstanceCutoff>(entity)
.map_or(0.0, |cutoff| cutoff.cutoff);
let joints_base = if mesh.skinned {
context.pack_joints()
} else {
0
};
let morph_base = if mesh.morph_targets > 0 {
context.pack_morph_weights(mesh.morph_targets)
} else {
0
};
let instance = MeshInstance::new(context.world_from_local, base_color)
.with_joints_base(joints_base)
.with_morph_base(morph_base)
.with_metadata_index(mesh.parts.metadata_index)
.with_cutoff(cutoff);
out.copy_from_slice(instance.as_bytes());
}
}
pub trait MeshSourceUnlitExt {
fn register_unlit_family(&mut self, world: &Scope<'_>);
fn allocate_unlit_mesh(
&mut self,
world: &Scope<'_>,
key: &UnlitPipelineKey,
desc: UnlitMeshDesc<'_>,
) -> GpuMesh;
fn allocate_unlit_material(
&mut self,
world: &Scope<'_>,
key: &UnlitPipelineKey,
view_id: ResHandle<TextureView>,
sampler_id: ResHandle<wgpu::Sampler>,
) -> GpuMaterial;
}
impl MeshSourceUnlitExt for MeshSource {
fn register_unlit_family(&mut self, world: &Scope<'_>) {
self.register_family::<UnlitPipelineKey, _, _>(world, UnlitFactory, UnlitInstance);
}
fn allocate_unlit_mesh(
&mut self,
world: &Scope<'_>,
key: &UnlitPipelineKey,
desc: UnlitMeshDesc<'_>,
) -> GpuMesh {
let device = self.device(world);
let queue = self.queue(world);
let UnlitMeshDesc {
positions,
uvs,
colors,
indices,
joints,
weights,
morph_deltas,
} = desc;
assert_eq!(
joints.is_some(),
weights.is_some(),
"a mesh's joints and weights come as a pair, so one cannot be present without the other"
);
let mut uv_color = UvColorFlags::empty();
if uvs.is_some() {
uv_color |= UvColorFlags::UV;
}
if colors.is_some() {
uv_color |= UvColorFlags::COLOR;
}
let channels = UnlitVertexChannels {
position: PositionStreamChannels {
position: Some(ChannelEncoding::CompressedPosition),
joints: joints.is_some() && weights.is_some(),
},
uv_color,
};
let variant = UnlitVariant {
options: key.options.clone(),
surface: SurfaceKey {
color_format: wgpu::TextureFormat::Rgba8UnormSrgb,
depth_stencil_format: None,
sample_count: 1,
},
channels,
base_color_texture: false,
morph: morph_deltas.is_some(),
strip_index_format: None,
};
let position_stream = variant.position_stream();
let uv_color_stream = variant.uv_color_stream();
let skinned = variant.needs_joints();
let (joints, weights) = if skinned {
let joints = joints.expect("a variant that reads joints needs the mesh's joints");
let weights = weights.expect("a variant that reads joints needs the mesh's weights");
assert_eq!(
joints.len(),
positions.len(),
"the joint stream must describe the same vertices as the positions"
);
assert_eq!(
weights.len(),
positions.len(),
"the weight stream must describe the same vertices as the positions"
);
(joints, weights)
} else {
(&[] as &[[u16; 4]], &[] as &[[f32; 4]])
};
let mut meta = MeshMetadata::default();
let vertex_count = positions.len();
let position_len = position_stream.byte_len(vertex_count);
let uv_color_len = uv_color_stream.byte_len(vertex_count);
let morph_deltas = variant.needs_morphs().then(|| {
let morph_deltas = morph_deltas.expect(
"a variant that reads morph positions needs the mesh's morph displacements",
);
assert_ne!(
morph_deltas.target_count, 0,
"a morphing mesh needs at least one target"
);
assert_eq!(
morph_deltas.deltas.len(),
positions.len() * morph_deltas.target_count as usize * 3,
"a morph displacement array must hold three components per vertex and target"
);
morph_deltas
});
let vertex_layouts = variant.vertex_buffer_layouts();
let layout_of = |slot: u32| -> VertexBufferLayoutDesc {
vertex_layouts
.get(slot as usize)
.and_then(|layout| layout.as_ref())
.cloned()
.unwrap_or(VertexBufferLayoutDesc {
array_stride: 0,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: VertexAttributes::new(),
})
};
let position_layout = layout_of(POSITION_SLOT);
let uv_color_layout = layout_of(UV_COLOR_SLOT);
let stream_layouts = [position_layout.clone(), uv_color_layout.clone()];
let vertices = self
.vertex_pool
.allocate(&device, &queue, &stream_layouts, vertex_count as u32)
.expect("the vertex pool grows with the mesh");
let vertex_offset = vertices.allocation.offset;
for (slot, layout, len) in [
(POSITION_SLOT, &position_layout, position_len),
(UV_COLOR_SLOT, &uv_color_layout, uv_color_len),
] {
if layout.array_stride == 0 {
continue;
}
let id = self.vertex_node(world, layout);
self.sync_vertex_node(world, &id, layout);
let buffer = self
.context
.graph(world)
.get(&id)
.expect("the stream's node exists")
.clone();
let mut view = queue
.write_buffer_with(
&buffer,
VertexStreamPool::byte_offset(layout, vertex_offset),
core::num::NonZeroU64::new(len as u64)
.expect("a declared stream is never empty"),
)
.expect("the stream's range fits its buffer");
if slot == POSITION_SLOT {
position_stream.write(
positions,
joints.iter().copied(),
compress_weights(weights),
&mut meta,
view.slice(..),
);
} else {
uv_color_stream.write(
uvs.unwrap_or(&[]),
colors.unwrap_or(&[]),
&mut meta,
view.slice(..),
);
}
}
let supports_base_vertex = self.capabilities(world).base_vertex();
assert!(
!supports_base_vertex || vertex_offset <= i32::MAX as u32,
"a vertex offset has to fit the i32 a draw's base vertex is"
);
let offset = IndexOffset::new(supports_base_vertex, vertex_offset);
let draw_base_vertex = offset.draw_base_vertex;
let (index_buffer, count, indexed, index_allocation, first) = match indices {
Some(indices) if !indices.is_empty() => {
let index_count = indices.len() as u32;
let (format, packed_len) = offset.packed_len(indices);
let range = self
.index_pool
.allocate(&device, &queue, packed_len as u32)
.expect("the index pool grows with the mesh");
{
let mut graph = self.context.graph(world);
MeshSource::sync_pool_node(&self.index_pool, &self.index_pool_id, &mut graph);
let buffer = graph
.get(&self.index_pool_id)
.expect("the index pool node exists")
.clone();
let mut view = queue
.write_buffer_with(
&buffer,
u64::from(range.allocation.offset),
core::num::NonZeroU64::new(packed_len as u64)
.expect("an empty index buffer is never allocated"),
)
.expect("the index range fits its buffer");
offset.write(indices, view.slice(..));
}
let first = range.allocation.offset / index_format_size(format);
(
Some((self.index_pool_id.clone(), format)),
index_count,
true,
Some(range.allocation),
first,
)
}
_ => (None, vertex_count as u32, false, None, vertex_offset),
};
let aabb = Aabb::new(meta.aabb_center, meta.aabb_half_extents);
let mut mesh = self.allocate_mesh_with_metadata(
world,
MeshDesc {
vertex_buffers: ArrayVec::new(),
index_buffer: None,
count,
indexed,
aabb,
bind_group: None,
morph_deltas,
},
meta,
skinned,
vertex_offset,
);
let parts = Rc::get_mut(&mut mesh.parts).expect("a fresh mesh handle is unshared");
let mut layouts = ArrayVec::<_, MAX_VERTEX_BUFFERS>::new();
for (slot, layout) in [
(POSITION_SLOT, &position_layout),
(UV_COLOR_SLOT, &uv_color_layout),
] {
if layout.array_stride > 0 {
layouts.push((slot, layout.clone()));
let node = self.vertex_node(world, layout);
parts.vertex_buffers.push((slot, node));
}
}
parts.index_buffer = index_buffer;
parts.index_allocation = index_allocation;
parts.vertex_allocation = Some(vertices.allocation);
mesh.vertex_layout = VertexLayout::new(layouts);
mesh.first = first;
mesh.base_vertex = draw_base_vertex;
mesh
}
fn allocate_unlit_material(
&mut self,
world: &Scope<'_>,
key: &UnlitPipelineKey,
view_id: ResHandle<TextureView>,
sampler_id: ResHandle<wgpu::Sampler>,
) -> GpuMaterial {
let layout = key.options.material_bind_group_layout(&self.device(world));
let device = self.device(world).clone();
let mut dependencies = ArrayVec::<ResHandle, 2>::new();
dependencies.push(view_id.erase());
dependencies.push(sampler_id.erase());
self.allocate_material(
world,
move |graph| {
let view = graph
.get(&view_id)
.expect("the view is in the graph")
.view()
.clone();
let sampler = graph
.get(&sampler_id)
.expect("the sampler is in the graph")
.clone();
let mut entries = ArrayVec::<wgpu::BindGroupEntry<'_>, 2>::new();
entries.push(wgpu::BindGroupEntry {
binding: BASE_COLOR_TEXTURE_BINDING,
resource: wgpu::BindingResource::TextureView(&view),
});
entries.push(wgpu::BindGroupEntry {
binding: BASE_COLOR_SAMPLER_BINDING,
resource: wgpu::BindingResource::Sampler(&sampler),
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("unlit3d::material::bind_group"),
layout: &layout,
entries: &entries,
})
},
dependencies,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_instance_color_is_white() {
assert_eq!(InstanceColor::default().color, glam::Vec4::ONE);
}
#[test]
fn a_strip_pipeline_declares_the_mesh_index_width() {
use wgpu::{IndexFormat, PrimitiveTopology as Topology};
assert_eq!(
strip_index_format(Topology::LineStrip, Some(IndexFormat::Uint16)),
Some(IndexFormat::Uint16)
);
assert_eq!(
strip_index_format(Topology::TriangleStrip, Some(IndexFormat::Uint32)),
Some(IndexFormat::Uint32)
);
assert_eq!(strip_index_format(Topology::LineStrip, None), None);
assert_eq!(strip_index_format(Topology::TriangleStrip, None), None);
}
#[test]
fn a_non_strip_pipeline_declares_no_index_width() {
use wgpu::{IndexFormat, PrimitiveTopology as Topology};
for topology in [
Topology::PointList,
Topology::LineList,
Topology::TriangleList,
] {
assert_eq!(
strip_index_format(topology, Some(IndexFormat::Uint16)),
None
);
assert_eq!(
strip_index_format(topology, Some(IndexFormat::Uint32)),
None
);
}
}
#[test]
fn a_device_with_base_vertex_puts_the_offset_on_the_draw() {
let offset = IndexOffset::new(true, 7);
assert_eq!(offset.draw_base_vertex, 7, "the draw carries the offset");
let (format, data) = offset.pack(&[0, 1, 2]);
assert_eq!(
format,
wgpu::IndexFormat::Uint16,
"the indices stay mesh-local, so they keep their width"
);
assert_eq!(packed_indices(format, &data, 3), vec![0, 1, 2]);
}
#[test]
fn a_device_without_base_vertex_bakes_the_offset_into_the_indices() {
let offset = IndexOffset::new(false, 7);
assert_eq!(offset.draw_base_vertex, 0, "the draw must not name one");
let (format, data) = offset.pack(&[0, 1, 2]);
assert_eq!(format, wgpu::IndexFormat::Uint16);
assert_eq!(packed_indices(format, &data, 3), vec![7, 8, 9]);
}
#[test]
fn a_mesh_at_the_start_of_the_pool_needs_no_offset_either_way() {
for supports_base_vertex in [true, false] {
let offset = IndexOffset::new(supports_base_vertex, 0);
assert_eq!(offset.baked, 0);
assert_eq!(offset.draw_base_vertex, 0);
let (format, data) = offset.pack(&[0, 1, 2]);
assert_eq!(format, wgpu::IndexFormat::Uint16);
assert_eq!(packed_indices(format, &data, 3), vec![0, 1, 2]);
}
}
#[test]
fn a_baked_offset_can_widen_the_indices() {
let indices = [0u32, 1, 2];
let offset = IndexOffset::new(false, u32::from(u16::MAX));
let (format, data) = offset.pack(&indices);
assert_eq!(
format,
wgpu::IndexFormat::Uint32,
"an index past `u16::MAX` cannot be narrowed"
);
assert_eq!(packed_indices(format, &data, 3), vec![65535, 65536, 65537]);
}
#[test]
fn packed_indices_are_padded_to_the_pool_alignment() {
let offset = IndexOffset::new(true, 0);
let (format, data) = offset.pack(&[0, 1, 2]);
assert_eq!(format, wgpu::IndexFormat::Uint16);
assert_eq!(
data.len(),
(3 * size_of::<u16>()).next_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT as usize)
);
assert!(
data[3 * size_of::<u16>()..].iter().all(|&byte| byte == 0),
"the padding is zeroed"
);
let indices: Vec<u32> = (0..(u16::MAX as u32 - 1)).collect();
let (format, data) = offset.pack(&indices);
assert_eq!(format, wgpu::IndexFormat::Uint16);
assert_eq!(
data.len(),
(indices.len() * size_of::<u16>())
.next_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT as usize)
);
}
fn packed_indices(format: wgpu::IndexFormat, bytes: &[u8], count: usize) -> Vec<u32> {
match format {
wgpu::IndexFormat::Uint16 => bytes[..count * size_of::<u16>()]
.as_chunks::<{ size_of::<u16>() }>()
.0
.iter()
.map(|index| u32::from(u16::from_ne_bytes(*index)))
.collect(),
wgpu::IndexFormat::Uint32 => bytes[..count * size_of::<u32>()]
.as_chunks::<{ size_of::<u32>() }>()
.0
.iter()
.map(|index| u32::from_ne_bytes(*index))
.collect(),
}
}
}