#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::components::sdf_programs::SdfPrograms;
use concinnity_core::components::sdf_volume::SdfVolume;
use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::gfx::render_types::LightUniforms;
use concinnity_core::platform::Platform;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::shader_programs::raymarch::{Family, ProxyFaces, VolumeFlags};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSString;
use objc2_foundation::ns_string;
use objc2_metal::{
MTLBlendFactor, MTLBlendOperation, MTLBlitCommandEncoder as _, MTLBuffer,
MTLCommandBuffer as _, MTLCommandEncoder as _, MTLCullMode, MTLDevice, MTLIndexType,
MTLLoadAction, MTLPixelFormat, MTLPrimitiveType, MTLRenderCommandEncoder as _,
MTLRenderPassDescriptor, MTLRenderPipelineDescriptor, MTLRenderPipelineState,
MTLResourceOptions, MTLStoreAction, MTLVertexFormat, MTLVertexStepFunction,
};
pub(in crate::metal) use concinnity_core::render::uniforms::{
RaymarchShadowCascade, RaymarchView, RaymarchVolumeUniforms,
};
use super::context::MtlContext;
use super::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use super::encode::RenderEncode;
use super::error::allocation_failed;
use super::scoped_encoder::ScopedEncoder;
mod swap;
const RAYMARCH_VERTEX_BUFFER: usize = 5;
type RaymarchLightsGpu = LightUniforms;
pub(in crate::metal) struct RaymarchVolumeRecord {
pub(in crate::metal) label: String,
pub(in crate::metal) pipeline: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(in crate::metal) front_pipeline:
Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(in crate::metal) shadow_pipeline:
Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(in crate::metal) prepass_pipelines: Option<FacePipelines>,
pub(in crate::metal) uniforms: RaymarchVolumeUniforms,
pub(in crate::metal) visible: bool,
pub(in crate::metal) volumetric: bool,
pub(in crate::metal) cast_shadows: bool,
pub(in crate::metal) refractive: bool,
pub(in crate::metal) world_center: [f32; 3],
pub(in crate::metal) world_extent: [f32; 3],
}
pub(in crate::metal) struct FacePipelines {
pub(in crate::metal) front: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(in crate::metal) back: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
}
impl FacePipelines {
fn get(&self, faces: ProxyFaces) -> &ProtocolObject<dyn MTLRenderPipelineState> {
match faces {
ProxyFaces::Front => &self.front,
ProxyFaces::Back => &self.back,
}
}
}
fn cull_mode(faces: ProxyFaces) -> MTLCullMode {
match faces {
ProxyFaces::Front => MTLCullMode::Front,
ProxyFaces::Back => MTLCullMode::Back,
}
}
impl RaymarchVolumeRecord {
fn faces(&self, view: &RaymarchView) -> ProxyFaces {
if self.volumetric {
ProxyFaces::Back
} else {
ProxyFaces::for_box(view, self.world_center, self.world_extent)
}
}
fn draw_pipeline(&self, faces: ProxyFaces) -> &ProtocolObject<dyn MTLRenderPipelineState> {
match (faces, &self.front_pipeline) {
(ProxyFaces::Front, Some(front)) => front,
_ => &self.pipeline,
}
}
}
pub(in crate::metal) fn volume_in_frustum(
center: [f32; 3],
extent: [f32; 3],
frustum: &Frustum,
) -> bool {
let min = [
center[0] - extent[0],
center[1] - extent[1],
center[2] - extent[2],
];
let max = [
center[0] + extent[0],
center[1] + extent[1],
center[2] + extent[2],
];
frustum.intersects_aabb(min, max)
}
fn entry_function(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
family: Family,
entry: &str,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLFunction>>> {
let msl = crate::shader::raymarch_source::artifact(
programs,
&crate::shader::raymarch_source::Request {
family,
platform: Platform::Metal,
entry,
hot_reload,
label: asset_label,
},
crate::shader::compile::cooked,
)?;
super::msl_cache::cooked_function(device, &msl, entry, &format!("SdfVolume '{asset_label}'"))
}
pub(in crate::metal) fn build_raymarch_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
faces: ProxyFaces,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let entry = |name| {
entry_function(
device,
programs,
Family::Surface,
name,
hot_reload,
asset_label,
)
};
let vert_fn = entry("raymarch_vertex")?;
let frag_fn = entry(face_fragment(faces, Family::Surface)?)?;
let vert_desc = vertex_descriptor(
&[VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: RAYMARCH_VERTEX_BUFFER,
}],
&[VertexLayout {
buffer_index: RAYMARCH_VERTEX_BUFFER,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(false);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
RenderError::ShaderCompile(format!(
"raymarch pipeline state for SdfVolume '{asset_label}': {e:?}"
))
})
}
pub(in crate::metal) fn build_raymarch_shadow_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let entry = |name| {
entry_function(
device,
programs,
Family::Shadow,
name,
hot_reload,
asset_label,
)
};
let vert_fn = entry("raymarch_shadow_vertex")?;
let frag_fn = entry("raymarch_shadow_fragment")?;
let vert_desc = vertex_descriptor(
&[VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: RAYMARCH_VERTEX_BUFFER,
}],
&[VertexLayout {
buffer_index: RAYMARCH_VERTEX_BUFFER,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
RenderError::ShaderCompile(format!(
"raymarch shadow pipeline state for SdfVolume '{asset_label}': {e:?}"
))
})
}
pub(in crate::metal) fn build_raymarch_volumetric_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let entry = |name| {
entry_function(
device,
programs,
Family::Volumetric,
name,
hot_reload,
asset_label,
)
};
let vert_fn = entry("raymarch_volumetric_vertex")?;
let frag_fn = entry("raymarch_volumetric_fragment")?;
let vert_desc = vertex_descriptor(
&[VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: RAYMARCH_VERTEX_BUFFER,
}],
&[VertexLayout {
buffer_index: RAYMARCH_VERTEX_BUFFER,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setRgbBlendOperation(MTLBlendOperation::Add);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::One);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setAlphaBlendOperation(MTLBlendOperation::Add);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
RenderError::ShaderCompile(format!(
"raymarch volumetric pipeline state for SdfVolume '{asset_label}': {e:?}"
))
})
}
pub(in crate::metal) fn build_raymarch_prepass_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
faces: ProxyFaces,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let entry = |name| {
entry_function(
device,
programs,
Family::Prepass,
name,
hot_reload,
asset_label,
)
};
let vert_fn = entry("raymarch_prepass_vertex")?;
let frag_fn = entry(face_fragment(faces, Family::Prepass)?)?;
let vert_desc = vertex_descriptor(
&[VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: RAYMARCH_VERTEX_BUFFER,
}],
&[VertexLayout {
buffer_index: RAYMARCH_VERTEX_BUFFER,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(&vert_fn));
desc.setFragmentFunction(Some(&frag_fn));
desc.setRasterSampleCount(1);
unsafe {
let targets = desc.colorAttachments();
for (i, format) in super::post::gbuffer::GBUFFER_FORMATS.iter().enumerate() {
let target = targets.objectAtIndexedSubscript(i);
target.setPixelFormat(*format);
target.setBlendingEnabled(false);
}
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
RenderError::ShaderCompile(format!(
"raymarch pre-pass pipeline state for SdfVolume '{asset_label}': {e:?}"
))
})
}
fn face_fragment(faces: ProxyFaces, family: Family) -> RenderResult<&'static str> {
faces
.fragment(family)
.ok_or_else(|| RenderError::Other(format!("no {faces:?}-face entry for {family:?}")))
}
pub(in crate::metal) struct VolumePipelines {
pub(in crate::metal) pipeline: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(in crate::metal) front_pipeline:
Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(in crate::metal) shadow_pipeline:
Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(in crate::metal) prepass_pipelines: Option<FacePipelines>,
}
pub(in crate::metal) fn build_volume_pipelines(
device: &ProtocolObject<dyn MTLDevice>,
programs: &SdfPrograms,
flags: VolumeFlags,
hot_reload: bool,
asset_label: &str,
) -> RenderResult<VolumePipelines> {
let surface = |faces| build_raymarch_pipeline(device, programs, faces, hot_reload, asset_label);
let prepass =
|faces| build_raymarch_prepass_pipeline(device, programs, faces, hot_reload, asset_label);
let (pipeline, front_pipeline) = if flags.volumetric {
let medium = build_raymarch_volumetric_pipeline(device, programs, hot_reload, asset_label)?;
(medium, None)
} else {
(
surface(ProxyFaces::Back)?,
Some(surface(ProxyFaces::Front)?),
)
};
let shadow_pipeline = if flags.casts() {
Some(build_raymarch_shadow_pipeline(
device,
programs,
hot_reload,
asset_label,
)?)
} else {
None
};
let prepass_pipelines = if flags.volumetric {
None
} else {
Some(FacePipelines {
front: prepass(ProxyFaces::Front)?,
back: prepass(ProxyFaces::Back)?,
})
};
Ok(VolumePipelines {
pipeline,
front_pipeline,
shadow_pipeline,
prepass_pipelines,
})
}
pub(in crate::metal) fn build_raymarch_volume_record(
device: &ProtocolObject<dyn MTLDevice>,
volume: &SdfVolume,
payload: &[u8],
hot_reload: bool,
asset_label: &str,
) -> RenderResult<RaymarchVolumeRecord> {
let programs =
crate::shader::raymarch_source::decode(payload, asset_label).map_err(RenderError::Other)?;
let VolumePipelines {
pipeline,
front_pipeline,
shadow_pipeline,
prepass_pipelines,
} = build_volume_pipelines(
device,
&programs,
VolumeFlags::of(volume),
hot_reload,
asset_label,
)?;
Ok(RaymarchVolumeRecord {
label: asset_label.to_string(),
pipeline,
front_pipeline,
shadow_pipeline,
prepass_pipelines,
uniforms: volume_uniforms_from(volume),
visible: volume.visible,
volumetric: volume.volumetric,
cast_shadows: volume.cast_shadows,
refractive: crate::shader::raymarch_source::taps_scene(&programs),
world_center: volume.center,
world_extent: volume.extent,
})
}
fn volume_uniforms_from(volume: &SdfVolume) -> RaymarchVolumeUniforms {
RaymarchVolumeUniforms {
center: volume.center,
_pad0: 0.0,
extent: volume.extent,
_pad1: 0.0,
cone_ratio: volume.cone_ratio(),
max_distance: volume.max_distance,
max_steps: volume.max_steps as i32,
receive_shadows: if volume.receive_shadows { 1 } else { 0 },
params: volume.params,
}
}
type RaymarchCubeBuffers = (
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
);
pub(in crate::metal) fn build_raymarch_cube_buffers(
device: &ProtocolObject<dyn MTLDevice>,
) -> RenderResult<RaymarchCubeBuffers> {
#[rustfmt::skip]
let corners: [Vertex; 8] = [
v([-1.0, -1.0, -1.0]),
v([ 1.0, -1.0, -1.0]),
v([ 1.0, 1.0, -1.0]),
v([-1.0, 1.0, -1.0]),
v([-1.0, -1.0, 1.0]),
v([ 1.0, -1.0, 1.0]),
v([ 1.0, 1.0, 1.0]),
v([-1.0, 1.0, 1.0]),
];
#[rustfmt::skip]
let indices: [u16; 36] = [
0, 2, 1, 0, 3, 2,
4, 5, 6, 4, 6, 7,
0, 4, 7, 0, 7, 3,
1, 2, 6, 1, 6, 5,
0, 1, 5, 0, 5, 4,
3, 7, 6, 3, 6, 2,
];
let vb_bytes = std::mem::size_of_val(&corners);
let ib_bytes = std::mem::size_of_val(&indices);
let vb = unsafe {
let ptr = std::ptr::NonNull::new(corners.as_ptr() as *mut _)
.ok_or_else(|| RenderError::Other("raymarch cube vertex pointer null".into()))?;
device
.newBufferWithBytes_length_options(ptr, vb_bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("the raymarch cube vertex buffer"))?
};
let ib = unsafe {
let ptr = std::ptr::NonNull::new(indices.as_ptr() as *mut _)
.ok_or_else(|| RenderError::Other("raymarch cube index pointer null".into()))?;
device
.newBufferWithBytes_length_options(ptr, ib_bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("the raymarch cube index buffer"))?
};
Ok((vb, ib))
}
fn v(pos: [f32; 3]) -> Vertex {
Vertex {
pos,
normal: [0.0, 0.0, 0.0],
tangent: [0.0, 0.0, 0.0],
color: [0.0, 0.0, 0.0],
uv: [0.0, 0.0],
}
}
impl MtlContext {
pub(in crate::metal) fn encode_raymarch(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
view: &RaymarchView,
frustum: &Frustum,
) -> RenderResult<u32> {
if self.raymarch.volumes.is_empty() {
return Ok(0);
}
let visible: Vec<bool> = self
.raymarch
.volumes
.iter()
.map(|v| v.visible && volume_in_frustum(v.world_center, v.world_extent, frustum))
.collect();
if !visible.iter().any(|&v| v) {
return Ok(0);
}
let vbuf = self
.raymarch
.cube_vertex_buffer
.as_ref()
.ok_or_else(|| RenderError::Other("raymarch cube vertex buffer missing".into()))?;
let ibuf = self
.raymarch
.cube_index_buffer
.as_ref()
.ok_or_else(|| RenderError::Other("raymarch cube index buffer missing".into()))?;
let depth_sampler = self.composite.sampler.as_ref();
let lights_gpu: RaymarchLightsGpu = self.light_uniforms;
let shadow_uniforms = self.shadow.uniforms;
let refractive = self
.raymarch
.volumes
.iter()
.zip(&visible)
.any(|(v, &vis)| vis && v.refractive);
{
let blit = cmd_buf.blitCommandEncoder().ok_or_else(|| {
RenderError::Other("failed to get raymarch scene-copy blit encoder".into())
})?;
blit.pushDebugGroup(&NSString::from_str("raymarch_scene_copy"));
unsafe {
if refractive {
blit.copyFromTexture_toTexture(
self.targets.hdr.hdr_resolve.as_ref(),
self.targets.hdr.hdr_resolve_copy.as_ref(),
);
}
blit.copyFromTexture_toTexture(
self.targets.hdr.depth_resolve.as_ref(),
self.targets.hdr.depth_copy.as_ref(),
);
}
blit.popDebugGroup();
blit.endEncoding();
}
let pass_desc = MTLRenderPassDescriptor::new();
unsafe {
let ca = pass_desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setTexture(Some(self.targets.hdr.hdr_resolve.as_ref()));
ca.setLoadAction(MTLLoadAction::Load);
ca.setStoreAction(MTLStoreAction::Store);
let da = pass_desc.depthAttachment();
da.setTexture(Some(self.targets.hdr.depth_resolve.as_ref()));
da.setLoadAction(MTLLoadAction::Load);
da.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&pass_desc, super::pass_timing::PassId::Raymarch);
}
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&pass_desc)
.ok_or_else(|| {
RenderError::Other("failed to get raymarch render encoder".into())
})?,
ns_string!("raymarch"),
);
enc.set_depth_stencil(self.targets.depth_state_inclusive.as_ref());
enc.set_vertex_value(view, 0);
enc.set_fragment_value(view, 0);
enc.set_fragment_value(&lights_gpu, 2);
enc.set_fragment_value(&shadow_uniforms, 3);
enc.set_vertex_buffer(vbuf, 0, RAYMARCH_VERTEX_BUFFER);
enc.set_fragment_texture(self.targets.hdr.depth_copy.as_ref(), 0);
enc.set_fragment_texture(self.shadow.map.as_ref(), 1);
enc.set_fragment_texture(self.scene.env_map.irradiance.as_ref(), 2);
enc.set_fragment_texture(self.scene.env_map.prefilter.as_ref(), 3);
enc.set_fragment_texture(self.targets.hdr.hdr_resolve_copy.as_ref(), 4);
enc.set_fragment_sampler(self.shadow.sampler.as_ref(), 0);
enc.set_fragment_sampler(self.scene.cube_sampler.as_ref(), 1);
enc.set_fragment_sampler(depth_sampler, 2);
let mut draws: u32 = 0;
for (i, vol) in self.raymarch.volumes.iter().enumerate() {
if !visible[i] {
continue;
}
let faces = vol.faces(view);
enc.setCullMode(cull_mode(faces));
enc.set_pipeline(vol.draw_pipeline(faces));
if vol.volumetric {
enc.set_depth_stencil(self.targets.depth_state_read_only.as_ref());
} else {
enc.set_depth_stencil(self.targets.depth_state_inclusive.as_ref());
}
enc.set_vertex_value(&vol.uniforms, 1);
enc.set_fragment_value(&vol.uniforms, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
36,
MTLIndexType::UInt16,
ibuf,
0,
);
}
draws += 1;
}
Ok(draws)
}
pub(in crate::metal) fn encode_raymarch_prepass(
&self,
enc: &ProtocolObject<dyn objc2_metal::MTLRenderCommandEncoder>,
view: &RaymarchView,
frustum: &Frustum,
) -> u32 {
let (Some(vbuf), Some(ibuf)) = (
self.raymarch.cube_vertex_buffer.as_ref(),
self.raymarch.cube_index_buffer.as_ref(),
) else {
return 0;
};
let mut draws: u32 = 0;
for vol in &self.raymarch.volumes {
let Some(pipelines) = vol.prepass_pipelines.as_ref() else {
continue;
};
if !vol.visible || !volume_in_frustum(vol.world_center, vol.world_extent, frustum) {
continue;
}
if draws == 0 {
enc.set_depth_stencil(self.targets.depth_state_inclusive.as_ref());
enc.set_vertex_value(view, 0);
enc.set_fragment_value(view, 0);
enc.set_fragment_value(&self.light_uniforms, 2);
enc.set_fragment_value(&self.shadow.uniforms, 3);
enc.set_vertex_buffer(vbuf, 0, RAYMARCH_VERTEX_BUFFER);
}
let faces = vol.faces(view);
enc.setCullMode(cull_mode(faces));
enc.set_pipeline(pipelines.get(faces));
enc.set_vertex_value(&vol.uniforms, 1);
enc.set_fragment_value(&vol.uniforms, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
36,
MTLIndexType::UInt16,
ibuf,
0,
);
}
draws += 1;
}
if draws > 0 {
enc.setCullMode(MTLCullMode::None);
}
draws
}
pub(in crate::metal) fn any_raymarch_shadow_casters(&self) -> bool {
self.raymarch
.volumes
.iter()
.any(|v| v.visible && v.cast_shadows && v.shadow_pipeline.is_some())
}
pub(in crate::metal) fn encode_sdf_shadow_casters(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
view: &RaymarchView,
) -> RenderResult<u32> {
use concinnity_core::gfx::render_types::NUM_SHADOW_CASCADES;
if !self.any_raymarch_shadow_casters() {
return Ok(0);
}
let vbuf = self.raymarch.cube_vertex_buffer.as_ref().ok_or_else(|| {
RenderError::Other("raymarch shadow: cube vertex buffer missing".into())
})?;
let ibuf = self.raymarch.cube_index_buffer.as_ref().ok_or_else(|| {
RenderError::Other("raymarch shadow: cube index buffer missing".into())
})?;
let lights_gpu: RaymarchLightsGpu = self.light_uniforms;
let shadow_uniforms = self.shadow.uniforms;
let mut draws: u32 = 0;
let render_mask = if self.shadow.render_mask == 0 {
(1u32 << NUM_SHADOW_CASCADES) - 1
} else {
self.shadow.render_mask
};
for cascade_idx in 0..NUM_SHADOW_CASCADES {
if render_mask & (1u32 << cascade_idx) == 0 {
continue;
}
let pass_desc = MTLRenderPassDescriptor::new();
let da = pass_desc.depthAttachment();
da.setTexture(Some(self.shadow.map.as_ref()));
da.setSlice(cascade_idx);
da.setLoadAction(MTLLoadAction::Load);
da.setStoreAction(MTLStoreAction::Store);
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&pass_desc)
.ok_or_else(|| {
RenderError::Other("failed to get raymarch shadow render encoder".into())
})?,
ns_string!("raymarch shadow"),
);
enc.setCullMode(cull_mode(ProxyFaces::Back));
enc.set_depth_stencil(self.targets.depth_state.as_ref());
let cascade = RaymarchShadowCascade {
cascade_idx: cascade_idx as u32,
_pad: [0; 3],
};
enc.set_fragment_value(view, 0);
enc.set_fragment_value(&lights_gpu, 2);
enc.set_vertex_value(&shadow_uniforms, 3);
enc.set_fragment_value(&shadow_uniforms, 3);
enc.set_vertex_value(&cascade, 4);
enc.set_fragment_value(&cascade, 4);
enc.set_vertex_buffer(vbuf, 0, RAYMARCH_VERTEX_BUFFER);
for vol in &self.raymarch.volumes {
if !vol.visible || !vol.cast_shadows {
continue;
}
let Some(pso) = vol.shadow_pipeline.as_ref() else {
continue;
};
enc.set_pipeline(pso);
enc.set_vertex_value(&vol.uniforms, 1);
enc.set_fragment_value(&vol.uniforms, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
36,
MTLIndexType::UInt16,
ibuf,
0,
);
}
draws += 1;
}
}
Ok(draws)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn volume_in_frustum_culls_offscreen_boxes() {
use concinnity_core::gfx::frustum::Frustum;
let identity = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let f = Frustum::from_camera(identity, None);
assert!(volume_in_frustum([0.0, 0.0, 0.0], [0.5, 0.5, 0.5], &f));
assert!(!volume_in_frustum([10.0, 0.0, 0.0], [0.5, 0.5, 0.5], &f));
assert!(volume_in_frustum(
[0.0, 0.0, 0.0],
[100.0, 100.0, 100.0],
&f
));
}
}