#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLRenderPipelineState, MTLResourceOptions};
use crate::components::{MAX_WATER_WAVES, WaterSurface, WaterWave};
use crate::geometry::water_grid::build_water_grid;
use crate::gfx::mesh_payload::Vertex;
use super::context::MtlContext;
use super::glass::build_transparent_pipeline_stages;
use super::slang_shaders;
use super::transparent::{TransparentDraw, bytes_of};
use concinnity_render::uniforms::TransparentView;
use concinnity_render::uniforms::{WATER_MAX_WAVES, WaterParams, WaterWaveGpu};
pub(in crate::metal) struct WaterSurfaceRecord {
pub(in crate::metal) vertex_buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
pub(in crate::metal) index_buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
pub(in crate::metal) index_count: u32,
pub(in crate::metal) params: WaterParams,
pub(in crate::metal) visible: bool,
pub(in crate::metal) centre: [f32; 3],
pub(in crate::metal) planar_slot: Option<usize>,
}
pub(in crate::metal) fn build_water_surface_record(
device: &ProtocolObject<dyn MTLDevice>,
surface: &WaterSurface,
) -> Result<WaterSurfaceRecord, String> {
let (verts, idxs) =
build_water_grid(surface.extent[0], surface.extent[1], surface.subdivisions)?;
let packed: Vec<Vertex> = verts
.into_iter()
.map(|(pos, normal, color, uv)| Vertex {
pos,
normal,
tangent: [1.0, 0.0, 0.0],
color,
uv,
})
.collect();
let vb_bytes = packed.len() * std::mem::size_of::<Vertex>();
let ib_bytes = idxs.len() * std::mem::size_of::<u16>();
let vb = unsafe {
let ptr = std::ptr::NonNull::new(packed.as_ptr() as *mut _)
.ok_or("water vertex buffer: source pointer is null")?;
device
.newBufferWithBytes_length_options(ptr, vb_bytes, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate water vertex buffer")?
};
let ib = unsafe {
let ptr = std::ptr::NonNull::new(idxs.as_ptr() as *mut _)
.ok_or("water index buffer: source pointer is null")?;
device
.newBufferWithBytes_length_options(ptr, ib_bytes, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate water index buffer")?
};
Ok(WaterSurfaceRecord {
vertex_buffer: vb,
index_buffer: ib,
index_count: idxs.len() as u32,
params: water_params_from(surface),
visible: surface.visible,
centre: surface.centre,
planar_slot: None,
})
}
fn water_params_from(surface: &WaterSurface) -> WaterParams {
let mut waves = [WaterWaveGpu::default(); WATER_MAX_WAVES];
for (slot, src) in waves.iter_mut().zip(surface.waves.iter()) {
*slot = wave_to_gpu(src);
}
WaterParams {
centre: [surface.centre[0], surface.centre[1], surface.centre[2], 0.0],
deep_colour: [
surface.deep_colour[0],
surface.deep_colour[1],
surface.deep_colour[2],
0.0,
],
shallow_colour: [
surface.shallow_colour[0],
surface.shallow_colour[1],
surface.shallow_colour[2],
0.0,
],
depth_falloff: surface.depth_falloff_metres,
foam_width: surface.foam_width_metres,
foam_intensity: surface.foam_intensity,
fresnel_power: surface.fresnel_power,
roughness: surface.roughness,
refraction_strength: surface.refraction_strength,
wave_count: surface.waves.len().min(MAX_WATER_WAVES) as u32,
_pad: 0.0,
waves,
planar: [0.0; 4],
}
}
const PLANAR_DISTORTION: f32 = 0.03;
fn wave_to_gpu(w: &WaterWave) -> WaterWaveGpu {
WaterWaveGpu {
dir_amp_wave: [w.direction[0], w.direction[1], w.amplitude, w.wavelength],
speed_steep_pad: [w.speed, w.steepness, 0.0, 0.0],
}
}
impl MtlContext {
pub(in crate::metal) fn water_planar_slot_live(&self) -> bool {
self.water
.surfaces
.iter()
.any(|s| s.visible && s.planar_slot.is_some())
}
pub(in crate::metal) fn collect_water_transparent_draws(
&self,
view: &TransparentView,
bindless: bool,
planar_live: bool,
out: &mut Vec<TransparentDraw>,
) {
let rt_on = self.rt.accel.is_some();
let pipeline = match (
rt_on && bindless,
&self.water.pipeline_rt_textured,
rt_on,
&self.water.pipeline_rt,
) {
(true, Some(p), _, _) => p,
(_, _, true, Some(p)) => p,
_ => match &self.water.pipeline {
Some(p) => p,
None => return,
},
};
let cam = view.camera_pos;
let planar_set = self.planar_reflection.as_ref();
for surface in &self.water.surfaces {
if !surface.visible {
continue;
}
let mut params = surface.params;
let mut fragment_textures = vec![
(0, self.hdr_targets.transparent_scene_copy.clone()),
(1, self.hdr_targets.depth_resolve.clone()),
];
if planar_live
&& let Some(targets) = surface
.planar_slot
.and_then(|s| planar_set.and_then(|set| set.targets.get(s)))
{
params.planar = [1.0, PLANAR_DISTORTION, 0.0, 0.0];
fragment_textures.push((11, targets.resolve.clone()));
}
let c = surface.centre;
let sort_distance =
((c[0] - cam[0]).powi(2) + (c[1] - cam[1]).powi(2) + (c[2] - cam[2]).powi(2))
.sqrt();
out.push(TransparentDraw {
pipeline: pipeline.clone(),
vertex_buffer: surface.vertex_buffer.clone(),
index_buffer: surface.index_buffer.clone(),
index_count: surface.index_count,
index_type: objc2_metal::MTLIndexType::UInt16,
index_offset_bytes: 0,
base_vertex: 0,
params: bytes_of(¶ms),
fragment_textures,
fragment_samplers: vec![(0, self.post_sampler.clone())],
sort_distance,
});
}
}
}
pub(super) fn build_water_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
build_water_pipeline_slang(device, hot_reload, &slang_shaders::WATER_FRAG)
}
pub(super) fn build_water_pipeline_rt(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
build_water_pipeline_slang(device, hot_reload, &slang_shaders::WATER_FRAG_RT)
}
pub(super) fn build_water_pipeline_rt_textured(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
build_water_pipeline_slang(device, hot_reload, &slang_shaders::WATER_FRAG_RT_TEXTURED)
}
fn build_water_pipeline_slang(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
fragment: &slang_shaders::SlangLib,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let vert_fn = slang_shaders::entry_function(device, &slang_shaders::WATER_VERT, hot_reload)?;
let frag_fn = slang_shaders::entry_function(device, fragment, hot_reload)?;
build_transparent_pipeline_stages(device, &vert_fn, &frag_fn)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wave_to_gpu_packs_the_lanes() {
let w = WaterWave {
amplitude: 0.25,
wavelength: 3.0,
speed: 1.5,
direction: [0.6, -0.8],
steepness: 0.4,
};
let g = wave_to_gpu(&w);
assert_eq!(g.dir_amp_wave, [0.6, -0.8, 0.25, 3.0]);
assert_eq!(g.speed_steep_pad, [1.5, 0.4, 0.0, 0.0]);
}
#[test]
fn water_params_from_maps_fields() {
let surface = WaterSurface {
centre: [1.0, 2.0, 3.0],
deep_colour: [0.02, 0.05, 0.12],
shallow_colour: [0.1, 0.3, 0.4],
depth_falloff_metres: 3.0,
foam_width_metres: 0.2,
foam_intensity: 0.5,
fresnel_power: 4.0,
roughness: 0.08,
refraction_strength: 0.05,
waves: vec![WaterWave::default(), WaterWave::default()],
..Default::default()
};
let p = water_params_from(&surface);
assert_eq!(p.centre, [1.0, 2.0, 3.0, 0.0]);
assert_eq!(p.deep_colour, [0.02, 0.05, 0.12, 0.0]);
assert_eq!(p.shallow_colour, [0.1, 0.3, 0.4, 0.0]);
assert_eq!(p.depth_falloff, 3.0);
assert_eq!(p.foam_width, 0.2);
assert_eq!(p.foam_intensity, 0.5);
assert_eq!(p.fresnel_power, 4.0);
assert_eq!(p.roughness, 0.08);
assert_eq!(p.refraction_strength, 0.05);
assert_eq!(p.wave_count, 2);
assert_eq!(p.planar, [0.0; 4]);
}
#[test]
fn water_params_clamps_the_wave_count() {
let surface = WaterSurface {
waves: vec![WaterWave::default(); MAX_WATER_WAVES + 3],
..Default::default()
};
assert_eq!(
water_params_from(&surface).wave_count,
MAX_WATER_WAVES as u32
);
}
}