use core::num::NonZeroU64;
use bytemuck::{Pod, Zeroable};
use crate::gpu::{
DEPTH_FORMAT, SMALLEST_VALUES, buffer, pipeline_layout, sampled, sampler, uniform,
};
use crate::light::{GpuLight, MAX_LIGHTS};
use crate::math::{Mat4, UVec2, Vec3, Vec4};
use crate::mesh::{Frame, MeshPlane, Placement, Vertex, Weighted};
use crate::renderer::post::HDR_FORMAT;
use crate::renderer::shadows::{Cast, GpuMap, MAX_MAPS};
use crate::renderer::skybox::Lighting;
use crate::surface_style::{self, Declaration, DrawPass, SurfaceStyleId};
use crate::{Camera, Color, Error, Material, ReliefData};
const CUTOUT: f32 = -1.0;
const VIEWPOINT: u64 = size_of::<Cast>() as u64;
const INITIAL_JOINTS: usize = 256;
const ADDING: wgpu::BlendState = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 3] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2];
const INSTANCE_ATTRIBUTES: [wgpu::VertexAttribute; 11] = wgpu::vertex_attr_array![
3 => Float32x4, 4 => Float32x4, 5 => Float32x4, 6 => Float32x4, 7 => Float32x4,
8 => Float32x4, 11 => Float32x4, 10 => Uint32, 14 => Float32,
15 => Float32, 9 => Uint32,
];
const SKIN_ATTRIBUTES: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![12 => Uint32x4, 13 => Float32x4];
const VERTICES: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &VERTEX_ATTRIBUTES,
};
const INSTANCES: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: size_of::<GpuInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &INSTANCE_ATTRIBUTES,
};
const SKIN: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: size_of::<Weighted>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &SKIN_ATTRIBUTES,
};
const BUFFER_LAYOUTS: [wgpu::VertexBufferLayout<'static>; 2] = [VERTICES, INSTANCES];
const SKINNED_LAYOUTS: [wgpu::VertexBufferLayout<'static>; 3] = [VERTICES, INSTANCES, SKIN];
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub(crate) struct GpuInstance {
model: [Vec4; 3],
tint: Color,
params: Vec4,
window: Vec4,
plane: WorldPlane,
relief: Relief,
roughness: f32,
metallic: f32,
palette: u32,
}
impl GpuInstance {
pub(crate) fn new(
placement: Placement,
material: Material,
pass: DrawPass,
frame: Frame,
relief: Relief,
plane: WorldPlane,
) -> Self {
let rows = placement.transform().matrix().transpose();
let emissive = material.emission();
Self {
model: [rows.x_axis, rows.y_axis, rows.z_axis],
tint: material.tint(),
params: Vec4::new(
shading(&material, pass),
emissive.red,
emissive.green,
emissive.blue,
),
window: frame.lane(),
plane,
relief,
roughness: material.rough(),
metallic: material.metal(),
palette: 0,
}
}
pub(crate) fn posed(mut self, at: u32) -> Self {
self.palette = at;
self
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub(crate) struct Relief(u32);
impl Relief {
const NONE: Self = Self(0);
const NORMALS: Self = Self(1);
const SOLID: Self = Self(2);
pub(crate) fn of(relief: Option<&ReliefData>, faced: bool) -> Self {
match relief {
Some(relief) if faced && relief.deep() => Self::SOLID,
Some(_) => Self::NORMALS,
None => Self::NONE,
}
}
pub(crate) fn solid(self) -> bool {
self == Self::SOLID
}
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub(crate) struct WorldPlane(Vec4);
impl WorldPlane {
pub(crate) const NONE: Self = Self(Vec4::ZERO);
pub(crate) fn of(plane: MeshPlane) -> Self {
Self(plane.equation())
}
pub(crate) fn upright(level: Vec3, anchor: Vec3) -> Self {
Self(level.extend(-level.dot(anchor)))
}
pub(crate) fn lies(self) -> bool {
self != Self::NONE
}
}
fn shading(material: &Material, pass: DrawPass) -> f32 {
if pass == DrawPass::Cutout || material.cuts() {
CUTOUT - material.litness()
} else {
material.litness()
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct FrameUniform {
irradiance: [Vec4; 9],
sky_from_clip: Mat4,
eye: Vec3,
foreshortened: u32,
looking: Vec3,
lights: u32,
top_mip: f32,
sky_share: f32,
_padding: [u32; 2],
}
pub(crate) struct FrameBindings {
viewpoints: wgpu::Buffer,
frame: wgpu::Buffer,
lights: wgpu::Buffer,
maps: wgpu::Buffer,
palette: wgpu::Buffer,
joints: usize,
stride: wgpu::BufferAddress,
layout: wgpu::BindGroupLayout,
}
impl FrameBindings {
pub(crate) fn new(device: &wgpu::Device) -> Self {
let stride = wgpu::BufferAddress::from(device.limits().min_uniform_buffer_offset_alignment);
let viewpoints = buffer(
device,
"mirage-engine viewpoints",
stride * (1 + MAX_MAPS as wgpu::BufferAddress),
wgpu::BufferUsages::UNIFORM,
);
let frame = buffer(
device,
"mirage-engine frame",
size_of::<FrameUniform>() as wgpu::BufferAddress,
wgpu::BufferUsages::UNIFORM,
);
let lights = buffer(
device,
"mirage-engine lights",
(MAX_LIGHTS * size_of::<GpuLight>()) as wgpu::BufferAddress,
wgpu::BufferUsages::STORAGE,
);
let maps = buffer(
device,
"mirage-engine shadow maps",
(MAX_MAPS * size_of::<GpuMap>()) as wgpu::BufferAddress,
wgpu::BufferUsages::STORAGE,
);
let palette = palette(device, INITIAL_JOINTS);
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine frame"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(VIEWPOINT),
},
count: None,
},
uniform(1, wgpu::ShaderStages::VERTEX_FRAGMENT),
storage(2),
storage(3),
sampled(4, true),
sampler(5),
skinning(6),
],
});
Self {
viewpoints,
frame,
lights,
maps,
palette,
joints: INITIAL_JOINTS,
stride,
layout,
}
}
pub(crate) fn set_palette(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
matrices: &[Mat4],
) -> bool {
let grew = matrices.len() > self.joints;
if grew {
self.joints = matrices.len().next_power_of_two();
self.palette = palette(device, self.joints);
}
if !matrices.is_empty() {
queue.write_buffer(&self.palette, 0, bytemuck::cast_slice(matrices));
}
grew
}
pub(crate) fn set_frame(
&self,
queue: &wgpu::Queue,
camera: Camera,
size: UVec2,
lights: &[GpuLight],
sky: Lighting,
) {
let view = camera.view();
let looking = view.direction();
let aspect = size.x as f32 / size.y as f32;
queue.write_buffer(
&self.viewpoints,
0,
bytemuck::bytes_of(&Cast::camera(camera.view_projection(aspect), size)),
);
queue.write_buffer(
&self.frame,
0,
bytemuck::bytes_of(&FrameUniform {
irradiance: sky.irradiance,
sky_from_clip: camera.rays_from_clip(aspect),
eye: view.eye(),
foreshortened: u32::from(camera.foreshortened()),
looking,
lights: lights.len() as u32,
top_mip: sky.top_mip,
sky_share: sky.share,
_padding: [0; 2],
}),
);
queue.write_buffer(&self.lights, 0, bytemuck::cast_slice(lights));
}
pub(crate) fn set_casters(&self, queue: &wgpu::Queue, casters: impl Iterator<Item = Cast>) {
for (slot, cast) in casters.enumerate() {
let at = wgpu::BufferAddress::from(self.caster_offset(slot));
queue.write_buffer(&self.viewpoints, at, bytemuck::bytes_of(&cast));
}
}
pub(crate) fn set_maps(&self, queue: &wgpu::Queue, maps: &[GpuMap]) {
if maps.is_empty() {
return;
}
queue.write_buffer(&self.maps, 0, bytemuck::cast_slice(maps));
}
pub(crate) fn caster_offset(&self, slot: usize) -> wgpu::DynamicOffset {
((1 + slot as wgpu::BufferAddress) * self.stride) as wgpu::DynamicOffset
}
pub(crate) fn layout(&self) -> &wgpu::BindGroupLayout {
&self.layout
}
pub(crate) fn bind(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
) -> wgpu::BindGroup {
fn held(binding: u32, resource: wgpu::BindingResource<'_>) -> wgpu::BindGroupEntry<'_> {
wgpu::BindGroupEntry { binding, resource }
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine frame"),
layout: &self.layout,
entries: &[
held(
0,
wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.viewpoints,
offset: 0,
size: NonZeroU64::new(VIEWPOINT),
}),
),
held(1, self.frame.as_entire_binding()),
held(2, self.lights.as_entire_binding()),
held(3, self.maps.as_entire_binding()),
held(4, wgpu::BindingResource::TextureView(view)),
held(5, wgpu::BindingResource::Sampler(sampler)),
held(6, self.palette.as_entire_binding()),
],
})
}
}
pub(crate) struct Pipelines {
opaque: Placing,
cutout: Placing,
transparent: Placing,
additive: Placing,
sky: wgpu::RenderPipeline,
caster: Skinning,
sampled_caster: Skinning,
}
impl Pipelines {
pub(crate) fn new(
device: &wgpu::Device,
samples: u32,
uniforms: &wgpu::BindGroupLayout,
textures: &wgpu::BindGroupLayout,
shadows: &wgpu::BindGroupLayout,
) -> Self {
let shaders = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mirage-engine forward"),
source: wgpu::ShaderSource::Wgsl(surface_style::built_in().into()),
});
let layout = pipeline_layout(
device,
"mirage-engine forward",
&[Some(uniforms), Some(textures), Some(shadows)],
);
let (caster, sampled_caster) = casters(device, &shaders, uniforms, textures);
let covering = pipeline_layout(device, "mirage-engine skybox", &[Some(uniforms)]);
Self {
sky: sky(device, &shaders, &covering, samples),
opaque: forward(device, &shaders, &layout, samples, Variant::Opaque),
cutout: forward(device, &shaders, &layout, samples, Variant::Cutout),
transparent: forward(device, &shaders, &layout, samples, Variant::Transparent),
additive: forward(device, &shaders, &layout, samples, Variant::Additive),
caster,
sampled_caster,
}
}
pub(crate) fn opaque(&self) -> &Placing {
&self.opaque
}
pub(crate) fn cutout(&self) -> &Placing {
&self.cutout
}
pub(crate) fn transparent(&self) -> &Placing {
&self.transparent
}
pub(crate) fn additive(&self) -> &Placing {
&self.additive
}
pub(crate) fn sky(&self) -> &wgpu::RenderPipeline {
&self.sky
}
pub(crate) fn caster(&self) -> &Skinning {
&self.caster
}
pub(crate) fn sampled_caster(&self) -> &Skinning {
&self.sampled_caster
}
}
pub(crate) struct Skinning {
plain: wgpu::RenderPipeline,
skinned: wgpu::RenderPipeline,
}
impl Skinning {
pub(crate) fn of(&self, skinned: bool) -> &wgpu::RenderPipeline {
match skinned {
true => &self.skinned,
false => &self.plain,
}
}
}
pub(crate) struct Placing {
placed: Skinning,
flat: Skinning,
faced: Skinning,
}
impl Placing {
pub(crate) fn of(&self, turn: Turn, skinned: bool) -> &wgpu::RenderPipeline {
let turned = match turn {
Turn::Placed => &self.placed,
Turn::Flat => &self.flat,
Turn::Faced => &self.faced,
};
turned.of(skinned)
}
}
pub(crate) struct Styles {
compiled: Vec<Compiled>,
}
impl Styles {
pub(crate) async fn compile(
device: &wgpu::Device,
declared: Vec<Declaration>,
samples: u32,
uniforms: &wgpu::BindGroupLayout,
textures: &wgpu::BindGroupLayout,
shadows: &wgpu::BindGroupLayout,
) -> Result<Self, Error> {
if declared.is_empty() {
return Ok(Self {
compiled: Vec::new(),
});
}
let mut compiled = Vec::with_capacity(declared.len());
let mut broken = Vec::new();
for declaration in declared {
let name = declaration.name;
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let style = compiled_style(device, declaration, samples, uniforms, textures, shadows);
match scope.pop().await {
Some(error) => broken.push(format!("{name}: {error}")),
None => compiled.push(style),
}
}
match broken.is_empty() {
true => Ok(Self { compiled }),
false => Err(Error::msg(format!(
"a style did not compile: {}",
broken.join("; ")
))),
}
}
pub(crate) fn ids(&self) -> impl Iterator<Item = SurfaceStyleId> {
(0..self.compiled.len() as u32).map(SurfaceStyleId)
}
pub(crate) fn drawn_with(
&self,
id: SurfaceStyleId,
turn: Turn,
skinned: bool,
) -> Option<(&wgpu::RenderPipeline, &wgpu::BindGroup)> {
let compiled = self.compiled.get(id.0 as usize)?;
Some((compiled.pipeline.of(turn, skinned), &compiled.bindings))
}
pub(crate) fn set_values(
&self,
queue: &wgpu::Queue,
id: SurfaceStyleId,
values: Option<&[u8]>,
) {
let Some(compiled) = self.compiled.get(id.0 as usize) else {
return;
};
let values = values.unwrap_or(&compiled.defaults);
if values.is_empty() {
return;
}
queue.write_buffer(&compiled.uniforms, 0, values);
}
}
struct Compiled {
pipeline: Placing,
uniforms: wgpu::Buffer,
bindings: wgpu::BindGroup,
defaults: Vec<u8>,
}
fn compiled_style(
device: &wgpu::Device,
declaration: Declaration,
samples: u32,
frame: &wgpu::BindGroupLayout,
textures: &wgpu::BindGroupLayout,
shadows: &wgpu::BindGroupLayout,
) -> Compiled {
let label = Some(declaration.name);
let shaders = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(declaration.source.into()),
});
let size = (declaration.defaults.len() as wgpu::BufferAddress).max(SMALLEST_VALUES);
let uniforms = buffer(
device,
"mirage-engine style",
size,
wgpu::BufferUsages::UNIFORM,
);
let values = values_layout(device, size);
let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
layout: &values,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
}],
});
let layout = pipeline_layout(
device,
declaration.name,
&[Some(frame), Some(textures), Some(shadows), Some(&values)],
);
Compiled {
pipeline: forward(device, &shaders, &layout, samples, declaration.pass.into()),
uniforms,
bindings,
defaults: declaration.defaults,
}
}
fn values_layout(device: &wgpu::Device, size: wgpu::BufferAddress) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine style"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(size),
},
count: None,
}],
})
}
#[derive(Clone, Copy)]
enum Variant {
Opaque,
Cutout,
Transparent,
Additive,
}
impl Variant {
fn entry(self) -> &'static str {
match self {
Self::Opaque | Self::Additive => "fragment",
Self::Cutout | Self::Transparent => "fragment_tested",
}
}
}
impl From<DrawPass> for Variant {
fn from(pass: DrawPass) -> Self {
match pass {
DrawPass::Opaque => Self::Opaque,
DrawPass::Cutout => Self::Cutout,
DrawPass::Translucent => Self::Transparent,
DrawPass::Additive => Self::Additive,
}
}
}
fn forward(
device: &wgpu::Device,
shaders: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
samples: u32,
variant: Variant,
) -> Placing {
let one = |turn| Skinning {
plain: drawn(device, shaders, layout, samples, variant, turn, false),
skinned: drawn(device, shaders, layout, samples, variant, turn, true),
};
Placing {
placed: one(Turn::Placed),
flat: one(Turn::Flat),
faced: one(Turn::Faced),
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum Turn {
Placed,
Flat,
Faced,
}
impl Turn {
pub(crate) fn of(faced: bool, plane: WorldPlane) -> Self {
match (faced, plane.lies()) {
(true, _) => Self::Faced,
(false, true) => Self::Flat,
(false, false) => Self::Placed,
}
}
pub(crate) fn lies(self) -> bool {
self != Self::Placed
}
fn stage(self) -> (&'static str, &'static str, &'static str) {
match self {
Self::Placed => ("", "", ""),
Self::Flat => (" flat", "", "flat_"),
Self::Faced => (" faced", "faced_", "faced_"),
}
}
}
fn skinned_stage(
skinned: bool,
) -> (
&'static str,
&'static str,
&'static [wgpu::VertexBufferLayout<'static>],
) {
match skinned {
true => (" skinned", "skinned_", &SKINNED_LAYOUTS),
false => ("", "", &BUFFER_LAYOUTS),
}
}
fn drawn(
device: &wgpu::Device,
shaders: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
samples: u32,
variant: Variant,
turn: Turn,
skinned: bool,
) -> wgpu::RenderPipeline {
let (turned, places, draws) = turn.stage();
let (skinning, blends, buffers) = skinned_stage(skinned);
let (label, blend, writes_depth) = match variant {
Variant::Opaque => ("mirage-engine forward", None, true),
Variant::Cutout => ("mirage-engine forward cutout", None, true),
Variant::Transparent => (
"mirage-engine forward blended",
Some(wgpu::BlendState::ALPHA_BLENDING),
false,
),
Variant::Additive => ("mirage-engine forward additive", Some(ADDING), false),
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&format!("{label}{turned}{skinning}")),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shaders,
entry_point: Some(&format!("{places}{blends}vertex")),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers,
},
primitive: wgpu::PrimitiveState {
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(writes_depth),
depth_compare: Some(match turn.lies() {
true => wgpu::CompareFunction::LessEqual,
false => wgpu::CompareFunction::Less,
}),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: samples,
..Default::default()
},
fragment: Some(wgpu::FragmentState {
module: shaders,
entry_point: Some(&format!("{draws}{}", variant.entry())),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
fn sky(
device: &wgpu::Device,
shaders: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
samples: u32,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mirage-engine skybox"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shaders,
entry_point: Some("sky_cover"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState {
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: samples,
..Default::default()
},
fragment: Some(wgpu::FragmentState {
module: shaders,
entry_point: Some("sky_draw"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
fn casters(
device: &wgpu::Device,
shaders: &wgpu::ShaderModule,
uniforms: &wgpu::BindGroupLayout,
textures: &wgpu::BindGroupLayout,
) -> (Skinning, Skinning) {
let depth_only = pipeline_layout(device, "mirage-engine shadow caster", &[Some(uniforms)]);
let cutout = pipeline_layout(
device,
"mirage-engine shadow caster cutout",
&[Some(uniforms), Some(textures)],
);
let both = |layout, stage, tested| Skinning {
plain: casting(device, shaders, layout, stage, tested, false),
skinned: casting(device, shaders, layout, stage, tested, true),
};
(
both(&depth_only, "caster", None),
both(&cutout, "sampling_caster", Some("caster_sampled")),
)
}
fn casting(
device: &wgpu::Device,
shaders: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
stage: &str,
tested: Option<&str>,
skinned: bool,
) -> wgpu::RenderPipeline {
let (skinning, blends, buffers) = skinned_stage(skinned);
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&format!("mirage-engine shadow {stage}{skinning}")),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shaders,
entry_point: Some(&format!("{blends}{stage}")),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers,
},
primitive: wgpu::PrimitiveState {
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
fragment: tested.map(|entry| wgpu::FragmentState {
module: shaders,
entry_point: Some(entry),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[],
}),
multiview_mask: None,
cache: None,
})
}
fn storage(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn skinning(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn palette(device: &wgpu::Device, joints: usize) -> wgpu::Buffer {
buffer(
device,
"mirage-engine palette",
(joints * size_of::<Mat4>()) as wgpu::BufferAddress,
wgpu::BufferUsages::STORAGE,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Vec3;
use crate::mesh::{Cube, Mesh};
use crate::{Transform, View};
const ANYWHERE: View = View::look_at(Vec3::Z, Vec3::ZERO);
fn placed(transform: impl Into<Transform>) -> Placement {
Cube.at::<()>(transform).record().placement(ANYWHERE)
}
#[test]
fn a_material_fills_the_lanes_the_shader_reads_it_from() {
let instance = GpuInstance::new(
placed(Vec3::new(1.0, 2.0, 3.0)),
Material::shaded(Color::rgb(0.25, 0.5, 0.75), 0.5).emissive(Color::rgb(2.0, 0.0, 4.0)),
DrawPass::Opaque,
Frame::default(),
Relief::of(None, false),
WorldPlane::NONE,
);
assert_eq!(instance.tint, Color::rgb(0.25, 0.5, 0.75));
assert_eq!(
instance.params,
Vec4::new(0.5, 2.0, 0.0, 4.0),
"litness first, then the surface's own emissive light"
);
assert_eq!(
(instance.roughness, instance.metallic),
(1.0, 0.0),
"and a surface that reflects only its base share of the sky, \
blurred to one color, at every angle"
);
assert_eq!(
instance.model.map(|row| row.w),
[1.0, 2.0, 3.0],
"the transposed rows carry the translation in their last lane"
);
assert_eq!(
instance.window,
Vec4::new(0.0, 0.0, 1.0, 1.0),
"and the whole texture is sampled where no part was asked for"
);
}
#[test]
fn a_cutting_draw_carries_its_litness_where_no_plain_one_reaches() {
let lane = |material: Material, pass| {
GpuInstance::new(
placed(Transform::IDENTITY),
material,
pass,
Frame::default(),
Relief::of(None, false),
WorldPlane::NONE,
)
.params
.x
};
let read_back = |shading: f32| {
if shading < 0.0 {
CUTOUT - shading
} else {
shading
}
};
for litness in [0.0, 0.5, 1.0] {
let painted = Material::shaded(Color::WHITE, litness);
let plain = lane(painted, DrawPass::Opaque);
let cutting = lane(painted.cutout(), DrawPass::Opaque);
let cut_by_pass = lane(painted, DrawPass::Cutout);
let cut_while_blended = lane(painted.cutout(), DrawPass::Translucent);
assert!(plain >= 0.0 && cutting < 0.0, "{plain} {cutting}");
assert_eq!(read_back(plain), litness);
assert_eq!(read_back(cutting), litness);
assert!(
cut_by_pass < 0.0 && read_back(cut_by_pass) == litness,
"a style's cutout pass cuts what its material never asked to"
);
assert!(
cut_while_blended < 0.0,
"and a blended draw keeps the cut its material asked for"
);
}
}
#[test]
fn an_instance_stays_the_size_the_buffer_layout_steps_by() {
assert_eq!(size_of::<GpuInstance>(), 128);
assert_eq!(
BUFFER_LAYOUTS[1].array_stride,
size_of::<GpuInstance>() as wgpu::BufferAddress
);
}
#[test]
fn the_shader_declares_every_stage_the_forward_pipelines_are_built_from() {
let shader = surface_style::built_in();
let declared = |entry: String| {
assert!(
shader.contains(&format!("fn {entry}(")),
"no stage of the shader is named {entry}"
);
};
for turn in [Turn::Placed, Turn::Flat, Turn::Faced] {
let (_, places, draws) = turn.stage();
for skinned in [false, true] {
let (_, blends, _) = skinned_stage(skinned);
declared(format!("{places}{blends}vertex"));
}
for variant in [
Variant::Opaque,
Variant::Cutout,
Variant::Transparent,
Variant::Additive,
] {
declared(format!("{draws}{}", variant.entry()));
}
}
for skinned in [false, true] {
let (_, blends, _) = skinned_stage(skinned);
declared(format!("{blends}caster"));
declared(format!("{blends}sampling_caster"));
}
declared("caster_sampled".to_owned());
}
}