mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
Merge main into IO-2/SHP-5/OUT-2/PRV-2 work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
//! The Advanced 3D rasteriser on wgpu: a render pipeline with a reversed-Z depth buffer
|
||||
//! (`Depth32Float`, Greater), the scene's materials, textures, lights and shadow maps in
|
||||
//! storage buffers, and `advanced3d.wgsl`'s physically based fragment shader. Opaque triangles
|
||||
//! are drawn with depth writes, then the sorted transparent ones blended over (premultiplied).
|
||||
//! Colour (`Rgba16Float`) and camera depth (`R32Float`) are read back for the CPU's resolve,
|
||||
//! depth of field and encoding.
|
||||
|
||||
use effectcraft_render::three_d::adv::{Scene, Target};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::context::{Enc, GpuContext};
|
||||
|
||||
/// Pipelines (built on first use).
|
||||
pub(crate) struct Pipes {
|
||||
bgl: wgpu::BindGroupLayout,
|
||||
opaque: wgpu::RenderPipeline,
|
||||
transparent: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
const COLOR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
const DEPTH_OUT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
|
||||
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
const VERTEX_SIZE: u64 = 52;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipes {
|
||||
fn new(device: &wgpu::Device) -> Pipes {
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("effectcraft advanced 3d"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/advanced3d.wgsl").into()),
|
||||
});
|
||||
let mut entries = vec![wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
|
||||
count: None,
|
||||
}];
|
||||
entries.extend((1..=6).map(storage));
|
||||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("advanced 3d"), entries: &entries });
|
||||
let layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("advanced 3d"), bind_group_layouts: &[Some(&bgl)], immediate_size: 0 });
|
||||
let attrs = [
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 0, shader_location: 0 },
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 12, shader_location: 1 },
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x2, offset: 24, shader_location: 2 },
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Float32x4, offset: 32, shader_location: 3 },
|
||||
wgpu::VertexAttribute { format: wgpu::VertexFormat::Uint32, offset: 48, shader_location: 4 },
|
||||
];
|
||||
let make = |transparent: bool| {
|
||||
let blend = transparent.then_some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING);
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(if transparent { "advanced 3d transparent" } else { "advanced 3d opaque" }),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout { array_stride: VERTEX_SIZE, step_mode: wgpu::VertexStepMode::Vertex, attributes: &attrs })],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: DEPTH,
|
||||
depth_write_enabled: Some(!transparent),
|
||||
depth_compare: Some(wgpu::CompareFunction::Greater),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[
|
||||
Some(wgpu::ColorTargetState { format: COLOR, blend, write_mask: wgpu::ColorWrites::ALL }),
|
||||
Some(wgpu::ColorTargetState {
|
||||
format: DEPTH_OUT,
|
||||
blend: None,
|
||||
write_mask: if transparent { wgpu::ColorWrites::empty() } else { wgpu::ColorWrites::ALL },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
Pipes { opaque: make(false), transparent: make(true), bgl }
|
||||
}
|
||||
}
|
||||
|
||||
fn f32s(v: impl IntoIterator<Item = f32>) -> Vec<u8> {
|
||||
let mut b: Vec<u8> = v.into_iter().flat_map(f32::to_le_bytes).collect();
|
||||
while b.len() < 16 {
|
||||
b.push(0);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Pack the scene's arrays as the shader's storage buffers.
|
||||
fn pack(s: &Scene) -> [Vec<u8>; 7] {
|
||||
let mut globals: Vec<f32> = vec![];
|
||||
// Column-major clip matrix.
|
||||
for c in 0..4 {
|
||||
for r in 0..4 {
|
||||
globals.push(s.clip[r][c]);
|
||||
}
|
||||
}
|
||||
globals.extend(s.view[2]);
|
||||
globals.extend([s.eye[0], s.eye[1], s.eye[2], s.ortho as u32 as f32]);
|
||||
globals.extend([s.cam_fwd[0], s.cam_fwd[1], s.cam_fwd[2], s.lights.len() as f32]);
|
||||
globals.extend([s.ambient[0], s.ambient[1], s.ambient[2], s.env.is_some() as u32 as f32]);
|
||||
let e = s.env.unwrap_or(effectcraft_render::three_d::adv::EnvInfo { radiance: 0, mips: 1, irradiance: 0, intensity: 0.0, rotation: 0.0 });
|
||||
globals.extend([e.radiance as f32, e.mips as f32, e.irradiance as f32, e.intensity]);
|
||||
let lit = !(s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]);
|
||||
globals.extend([e.rotation, lit as u32 as f32, 0.0, 0.0]);
|
||||
let mats = s.materials.iter().flat_map(|m| {
|
||||
let flags = (m.double_sided as u32) | (m.unlit as u32) << 1 | (m.accepts_lights as u32) << 2 | (m.receives_shadows as u32) << 3 | (m.alpha_mode << 8);
|
||||
[
|
||||
m.base[0],
|
||||
m.base[1],
|
||||
m.base[2],
|
||||
m.base[3],
|
||||
m.emissive[0],
|
||||
m.emissive[1],
|
||||
m.emissive[2],
|
||||
m.metallic,
|
||||
m.roughness,
|
||||
m.normal_scale,
|
||||
m.occlusion_strength,
|
||||
m.alpha_cutoff,
|
||||
m.tex_base as f32,
|
||||
m.tex_mr as f32,
|
||||
m.tex_normal as f32,
|
||||
m.tex_occlusion as f32,
|
||||
m.tex_emissive as f32,
|
||||
flags as f32,
|
||||
m.diffuse_k,
|
||||
m.specular_k,
|
||||
m.ambient_k,
|
||||
m.opacity,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
});
|
||||
let mut tex: Vec<u8> = s.textures.iter().flat_map(|t| [t.offset, t.width, t.height, t.wrap_u | t.wrap_v << 8]).flat_map(u32::to_le_bytes).collect();
|
||||
while tex.len() < 16 {
|
||||
tex.push(0);
|
||||
}
|
||||
let texels = f32s(s.texels.iter().flatten().copied());
|
||||
let lights = s.lights.iter().flat_map(|l| {
|
||||
[
|
||||
l.pos[0],
|
||||
l.pos[1],
|
||||
l.pos[2],
|
||||
l.kind as f32,
|
||||
l.dir[0],
|
||||
l.dir[1],
|
||||
l.dir[2],
|
||||
l.cos_outer,
|
||||
l.color[0],
|
||||
l.color[1],
|
||||
l.color[2],
|
||||
l.cos_inner,
|
||||
l.falloff as f32,
|
||||
l.radius,
|
||||
l.falloff_distance,
|
||||
l.shadow as f32,
|
||||
l.shadow_darkness,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
});
|
||||
let shadows = s.shadows.iter().flat_map(|m| {
|
||||
let mut v: Vec<f32> = m.view.iter().flatten().copied().collect();
|
||||
v.extend([m.focal, m.ortho as u32 as f32, f32::from_bits(m.size), f32::from_bits(m.offset), m.bias, m.radius, 0.0, 0.0]);
|
||||
v
|
||||
});
|
||||
[f32s(globals), f32s(mats), tex, texels, f32s(lights), f32s(shadows), f32s(s.shadow_texels.iter().copied())]
|
||||
}
|
||||
|
||||
fn half_to_f32(h: u16) -> f32 {
|
||||
let s = if h & 0x8000 != 0 { -1.0 } else { 1.0 };
|
||||
let e = ((h >> 10) & 0x1f) as i32;
|
||||
let m = (h & 0x3ff) as f32;
|
||||
match e {
|
||||
0 => s * m * 2f32.powi(-24),
|
||||
31 => {
|
||||
if m == 0.0 {
|
||||
s * f32::INFINITY
|
||||
} else {
|
||||
f32::NAN
|
||||
}
|
||||
}
|
||||
_ => s * (1.0 + m / 1024.0) * 2f32.powi(e - 15),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterise a scene. `None` when the device can't (no readback, size or buffer limits).
|
||||
pub(crate) fn render(g: &GpuContext, s: &Scene) -> Option<Target> {
|
||||
if !g.can_readback() || !g.fits(s.width, s.height) || s.indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let limits = g.device.limits();
|
||||
let bufs = pack(s);
|
||||
if bufs.iter().any(|b| b.len() as u64 > limits.max_storage_buffer_binding_size) {
|
||||
return None;
|
||||
}
|
||||
let p = g.adv3d.get_or_init(|| Pipes::new(&g.device));
|
||||
let dev = &g.device;
|
||||
let vb: Vec<u8> = s
|
||||
.vertices
|
||||
.iter()
|
||||
.flat_map(|v| {
|
||||
let mut b = Vec::with_capacity(VERTEX_SIZE as usize);
|
||||
for x in v.pos.iter().chain(&v.normal).chain(&v.uv).chain(&v.tangent) {
|
||||
b.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
b.extend_from_slice(&v.material.to_le_bytes());
|
||||
b
|
||||
})
|
||||
.collect();
|
||||
let vbuf = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d vertices"), contents: &vb, usage: wgpu::BufferUsages::VERTEX });
|
||||
let ib: Vec<u8> = s.indices.iter().flat_map(|i| i.to_le_bytes()).collect();
|
||||
let ibuf = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d indices"), contents: &ib, usage: wgpu::BufferUsages::INDEX });
|
||||
let ub = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d globals"), contents: &bufs[0], usage: wgpu::BufferUsages::UNIFORM });
|
||||
let sbs: Vec<wgpu::Buffer> = bufs[1..]
|
||||
.iter()
|
||||
.map(|b| dev.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("adv3d data"), contents: b, usage: wgpu::BufferUsages::STORAGE }))
|
||||
.collect();
|
||||
let mut entries = vec![wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() }];
|
||||
for (i, b) in sbs.iter().enumerate() {
|
||||
entries.push(wgpu::BindGroupEntry { binding: i as u32 + 1, resource: b.as_entire_binding() });
|
||||
}
|
||||
let bg = dev.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("adv3d"), layout: &p.bgl, entries: &entries });
|
||||
let tex = |format: wgpu::TextureFormat, usage: wgpu::TextureUsages| {
|
||||
dev.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("adv3d target"),
|
||||
size: wgpu::Extent3d { width: s.width, height: s.height, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let rt = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
|
||||
let color = tex(COLOR, rt);
|
||||
let zout = tex(DEPTH_OUT, rt);
|
||||
let depth = tex(DEPTH, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||
let (cv, zv, dv) = (color.create_view(&Default::default()), zout.create_view(&Default::default()), depth.create_view(&Default::default()));
|
||||
let mut enc = dev.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("adv3d") });
|
||||
{
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("adv3d"),
|
||||
color_attachments: &[
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view: &cv,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
||||
}),
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view: &zv,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: -1.0, g: 0.0, b: 0.0, a: 0.0 }), store: wgpu::StoreOp::Store },
|
||||
}),
|
||||
],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &dv,
|
||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), store: wgpu::StoreOp::Store }),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_bind_group(0, &bg, &[]);
|
||||
pass.set_vertex_buffer(0, vbuf.slice(..));
|
||||
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
|
||||
let n = s.indices.len() as u32;
|
||||
if s.opaque_count > 0 {
|
||||
pass.set_pipeline(&p.opaque);
|
||||
pass.draw_indexed(0..s.opaque_count, 0, 0..1);
|
||||
}
|
||||
if n > s.opaque_count {
|
||||
pass.set_pipeline(&p.transparent);
|
||||
pass.draw_indexed(s.opaque_count..n, 0, 0..1);
|
||||
}
|
||||
}
|
||||
g.queue.submit([enc.finish()]);
|
||||
let mut e = Enc::new(g);
|
||||
let cb = e.read_texture(&color, s.width, s.height, 8)?;
|
||||
let zb = e.read_texture(&zout, s.width, s.height, 4)?;
|
||||
let color = cb.chunks_exact(8).map(|c| [0, 1, 2, 3].map(|k| half_to_f32(u16::from_le_bytes([c[2 * k], c[2 * k + 1]])))).collect();
|
||||
let depth = zb
|
||||
.chunks_exact(4)
|
||||
.map(|c| {
|
||||
let z = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
|
||||
if z < 0.0 { f32::INFINITY } else { z }
|
||||
})
|
||||
.collect();
|
||||
Some(Target { width: s.width, height: s.height, color, depth })
|
||||
}
|
||||
@@ -94,6 +94,8 @@ pub struct GpuContext {
|
||||
dummy_tex: wgpu::TextureView,
|
||||
dummy_buf: wgpu::Buffer,
|
||||
uploads: Mutex<Uploads>,
|
||||
/// Advanced 3D render pipelines (built on first use).
|
||||
pub(crate) adv3d: std::sync::OnceLock<crate::adv3d::Pipes>,
|
||||
}
|
||||
|
||||
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
||||
@@ -225,6 +227,7 @@ impl GpuContext {
|
||||
dummy_tex: dummy.create_view(&Default::default()),
|
||||
dummy_buf,
|
||||
uploads: Mutex::new(Uploads::default()),
|
||||
adv3d: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
//! clamping and quantisation, and converts colour spaces. 3D runs, adjustment layers and
|
||||
//! wireframes run on the CPU between GPU steps (read back, draw, upload).
|
||||
//!
|
||||
//! Advanced 3D comps rasterise on a render pipeline (`advanced3d.wgsl`: depth buffer, PBR,
|
||||
//! image-based light, shadow maps), see [`Accelerator::raster_3d`].
|
||||
//!
|
||||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once.
|
||||
@@ -18,6 +21,7 @@
|
||||
//! skip readback entirely with [`Gpu::render_display`], which leaves an RGBA8 texture for
|
||||
//! egui-wgpu to draw.
|
||||
|
||||
mod adv3d;
|
||||
mod context;
|
||||
mod effects;
|
||||
mod ops;
|
||||
@@ -121,7 +125,13 @@ impl Accelerator for Gpu {
|
||||
fn effects(&self, chain: &[FxStep], buf: &Buf, levels: Option<f32>) -> Option<Buf> {
|
||||
effects::run_chain(&mut Enc::new(&self.ctx), chain, buf, levels)
|
||||
}
|
||||
|
||||
fn raster_3d(&self, scene: &effectcraft_render::three_d::adv::Scene) -> Option<effectcraft_render::three_d::adv::Target> {
|
||||
adv3d::render(&self.ctx, scene)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_adv3d;
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
// Advanced 3D: rasterised meshes with physically based shading. A step-for-step port of
|
||||
// `effectcraft_render::three_d::adv::shade` (Cook–Torrance GGX + Lambert, image-based light
|
||||
// from an equirectangular environment, PCF shadow maps); the CPU rasteriser is the reference.
|
||||
|
||||
struct Globals {
|
||||
clip: mat4x4<f32>,
|
||||
// Camera-space depth row (x, y, z, w).
|
||||
view_z: vec4<f32>,
|
||||
// eye.xyz, ortho (0/1)
|
||||
eye: vec4<f32>,
|
||||
// cam_fwd.xyz, light count
|
||||
fwd: vec4<f32>,
|
||||
// ambient.rgb, environment present (0/1)
|
||||
ambient: vec4<f32>,
|
||||
// radiance first texture, mip count, irradiance texture, intensity
|
||||
env: vec4<f32>,
|
||||
// rotation, any lighting (0/1), -, -
|
||||
env2: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> g: Globals;
|
||||
@group(0) @binding(1) var<storage, read> materials: array<vec4<f32>>;
|
||||
@group(0) @binding(2) var<storage, read> texinfo: array<vec4<u32>>;
|
||||
@group(0) @binding(3) var<storage, read> texels: array<vec4<f32>>;
|
||||
@group(0) @binding(4) var<storage, read> lights: array<vec4<f32>>;
|
||||
@group(0) @binding(5) var<storage, read> shadows: array<vec4<f32>>;
|
||||
@group(0) @binding(6) var<storage, read> shadow_texels: array<f32>;
|
||||
|
||||
const PI: f32 = 3.14159265358979;
|
||||
const MAT_STRIDE: u32 = 6u;
|
||||
const LIGHT_STRIDE: u32 = 5u;
|
||||
const SHADOW_STRIDE: u32 = 5u;
|
||||
|
||||
struct VIn {
|
||||
@location(0) pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) tangent: vec4<f32>,
|
||||
@location(4) material: u32,
|
||||
};
|
||||
|
||||
struct VOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) tangent: vec4<f32>,
|
||||
@location(4) @interpolate(flat) material: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs(v: VIn) -> VOut {
|
||||
var o: VOut;
|
||||
o.clip = g.clip * vec4<f32>(v.pos, 1.0);
|
||||
o.pos = v.pos;
|
||||
o.normal = v.normal;
|
||||
o.uv = v.uv;
|
||||
o.tangent = v.tangent;
|
||||
o.material = v.material;
|
||||
return o;
|
||||
}
|
||||
|
||||
fn norm3(a: vec3<f32>) -> vec3<f32> {
|
||||
let l = sqrt(dot(a, a));
|
||||
if (l > 1e-20) {
|
||||
return a / l;
|
||||
}
|
||||
return vec3<f32>(0.0, 0.0, -1.0);
|
||||
}
|
||||
|
||||
fn srgb_to_linear(c: f32) -> f32 {
|
||||
if (c <= 0.04045) {
|
||||
return c / 12.92;
|
||||
}
|
||||
return pow((c + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
fn wrap_i(i: i32, n: i32, mode: u32) -> i32 {
|
||||
if (mode == 1u) {
|
||||
return clamp(i, 0, n - 1);
|
||||
}
|
||||
if (mode == 2u) {
|
||||
let p = 2 * n;
|
||||
var m = i % p;
|
||||
if (m < 0) {
|
||||
m = m + p;
|
||||
}
|
||||
if (m >= n) {
|
||||
return p - 1 - m;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
var m = i % n;
|
||||
if (m < 0) {
|
||||
m = m + n;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
fn texel(t: vec4<u32>, x: i32, y: i32) -> vec4<f32> {
|
||||
let w = i32(t.y);
|
||||
let h = i32(t.z);
|
||||
let xi = wrap_i(x, w, t.w & 255u);
|
||||
let yi = wrap_i(y, h, (t.w >> 8u) & 255u);
|
||||
return texels[t.x + u32(yi * w + xi)];
|
||||
}
|
||||
|
||||
fn sample_tex(i: i32, uv: vec2<f32>) -> vec4<f32> {
|
||||
let t = texinfo[u32(i)];
|
||||
if (t.y == 0u || t.z == 0u) {
|
||||
return vec4<f32>(1.0);
|
||||
}
|
||||
let x = uv.x * f32(t.y) - 0.5;
|
||||
let y = uv.y * f32(t.z) - 0.5;
|
||||
let x0 = floor(x);
|
||||
let y0 = floor(y);
|
||||
let fx = x - x0;
|
||||
let fy = y - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = texel(t, xi, yi);
|
||||
let b = texel(t, xi + 1, yi);
|
||||
let c = texel(t, xi, yi + 1);
|
||||
let d = texel(t, xi + 1, yi + 1);
|
||||
let top = a + (b - a) * fx;
|
||||
let bot = c + (d - c) * fx;
|
||||
return top + (bot - top) * fy;
|
||||
}
|
||||
|
||||
fn d_ggx(n_h: f32, a: f32) -> f32 {
|
||||
let a2 = a * a;
|
||||
let d = n_h * n_h * (a2 - 1.0) + 1.0;
|
||||
return a2 / max(PI * d * d, 1e-12);
|
||||
}
|
||||
|
||||
fn g1(x: f32, k: f32) -> f32 {
|
||||
return x / (x * (1.0 - k) + k);
|
||||
}
|
||||
|
||||
fn brdf(base: vec3<f32>, metallic: f32, rough_in: f32, spec_k: f32, diff_k: f32, n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> vec3<f32> {
|
||||
let n_l = dot(n, l);
|
||||
if (n_l <= 0.0) {
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
let n_v = max(dot(n, v), 1e-4);
|
||||
let h = norm3(v + l);
|
||||
let n_h = max(dot(n, h), 0.0);
|
||||
let v_h = max(dot(v, h), 0.0);
|
||||
let r = clamp(rough_in, 0.03, 1.0);
|
||||
let f0 = mix(vec3<f32>(0.04 * spec_k), base, metallic);
|
||||
let fk = pow(clamp(1.0 - v_h, 0.0, 1.0), 5.0);
|
||||
let f = f0 + (vec3<f32>(1.0) - f0) * fk;
|
||||
let d = d_ggx(n_h, r * r);
|
||||
let k = (r + 1.0) * (r + 1.0) / 8.0;
|
||||
let gg = g1(n_v, k) * g1(n_l, k);
|
||||
let spec = d * gg / max(4.0 * n_v * n_l, 1e-4);
|
||||
let kd = (vec3<f32>(1.0) - f) * (1.0 - metallic);
|
||||
return (kd * base / PI * diff_k + f * spec) * n_l * PI;
|
||||
}
|
||||
|
||||
fn env_brdf(n_v: f32, rough: f32) -> vec2<f32> {
|
||||
let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
|
||||
let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
|
||||
let r = rough * c0 + c1;
|
||||
let a004 = min(r.x * r.x, exp2(-9.28 * n_v)) * r.x + r.y;
|
||||
return vec2<f32>(-1.04 * a004 + r.z, 1.04 * a004 + r.w);
|
||||
}
|
||||
|
||||
fn equirect_uv(d: vec3<f32>, rot: f32) -> vec2<f32> {
|
||||
let u = 0.5 + (atan2(d.x, d.z) + rot) / (2.0 * PI);
|
||||
let v = acos(clamp(-d.y, -1.0, 1.0)) / PI;
|
||||
return vec2<f32>(u - floor(u), v);
|
||||
}
|
||||
|
||||
fn shadow_factor(m: u32, p: vec3<f32>, n_l: f32) -> f32 {
|
||||
let r0 = shadows[m];
|
||||
let r1 = shadows[m + 1u];
|
||||
let r2 = shadows[m + 2u];
|
||||
let info = shadows[m + 3u];
|
||||
let extra = shadows[m + 4u];
|
||||
let q = vec3<f32>(dot(r0.xyz, p) + r0.w, dot(r1.xyz, p) + r1.w, dot(r2.xyz, p) + r2.w);
|
||||
let focal = info.x;
|
||||
let ortho = info.y > 0.5;
|
||||
let size = bitcast<u32>(info.z);
|
||||
let offset = bitcast<u32>(info.w);
|
||||
let c = f32(size) * 0.5;
|
||||
var u: f32;
|
||||
var v: f32;
|
||||
if (ortho) {
|
||||
u = q.x * focal + c;
|
||||
v = q.y * focal + c;
|
||||
} else {
|
||||
if (q.z <= 1e-3) {
|
||||
return 1.0;
|
||||
}
|
||||
u = q.x * focal / q.z + c;
|
||||
v = q.y * focal / q.z + c;
|
||||
}
|
||||
let z = q.z;
|
||||
let texel = select(abs(z) / focal, 1.0 / focal, ortho);
|
||||
let bias = extra.x * texel * (1.0 + extra.y) * (1.0 + 3.0 * (1.0 - clamp(n_l, 0.0, 1.0)));
|
||||
let step = extra.y / 2.0;
|
||||
let sz = i32(size);
|
||||
var lit = 0.0;
|
||||
for (var j = -2; j <= 2; j = j + 1) {
|
||||
for (var i = -2; i <= 2; i = i + 1) {
|
||||
let x = i32(floor(u + f32(i) * step));
|
||||
let y = i32(floor(v + f32(j) * step));
|
||||
if (x < 0 || y < 0 || x >= sz || y >= sz) {
|
||||
lit = lit + 1.0;
|
||||
continue;
|
||||
}
|
||||
let d = shadow_texels[offset + u32(y * sz + x)];
|
||||
if (z - bias <= d) {
|
||||
lit = lit + 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lit / 25.0;
|
||||
}
|
||||
|
||||
fn light_shadow(li: u32, kind: u32, lpos: vec3<f32>, shadow: i32, darkness: f32, p: vec3<f32>, n_l: f32) -> f32 {
|
||||
if (shadow < 0) {
|
||||
return 1.0;
|
||||
}
|
||||
var face = 0u;
|
||||
if (kind == 2u) {
|
||||
let d = p - lpos;
|
||||
let a = abs(d);
|
||||
if (a.x >= a.y && a.x >= a.z) {
|
||||
face = select(1u, 0u, d.x > 0.0);
|
||||
} else if (a.y >= a.z) {
|
||||
face = select(3u, 2u, d.y > 0.0);
|
||||
} else {
|
||||
face = select(5u, 4u, d.z > 0.0);
|
||||
}
|
||||
}
|
||||
let f = shadow_factor((u32(shadow) + face) * SHADOW_STRIDE, p, n_l);
|
||||
return 1.0 - darkness * (1.0 - f);
|
||||
}
|
||||
|
||||
fn attenuation(kind: u32, falloff: u32, radius: f32, falloff_distance: f32, cos_inner: f32, cos_outer: f32, dir: vec3<f32>, to_l: vec3<f32>, dist: f32) -> f32 {
|
||||
var k = 1.0;
|
||||
if (falloff == 1u && kind != 0u) {
|
||||
if (dist <= radius) {
|
||||
k = 1.0;
|
||||
} else if (falloff_distance <= 0.0) {
|
||||
k = 0.0;
|
||||
} else {
|
||||
let t = clamp((dist - radius) / falloff_distance, 0.0, 1.0);
|
||||
k = 1.0 - t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
} else if (falloff == 2u && kind != 0u) {
|
||||
if (dist <= radius || (radius <= 0.0 && dist <= 1.0)) {
|
||||
k = 1.0;
|
||||
} else {
|
||||
let q = max(radius, 1.0) / dist;
|
||||
k = q * q;
|
||||
}
|
||||
}
|
||||
if (kind == 1u) {
|
||||
let cos_a = clamp(dot(-to_l, dir), -1.0, 1.0);
|
||||
if (cos_a >= cos_inner) {
|
||||
k = k * 1.0;
|
||||
} else if (cos_a <= cos_outer) {
|
||||
k = 0.0;
|
||||
} else {
|
||||
let a = acos(cos_a);
|
||||
let ai = acos(cos_inner);
|
||||
let ao = acos(cos_outer);
|
||||
let t = clamp((a - ai) / max(ao - ai, 1e-6), 0.0, 1.0);
|
||||
k = k * (1.0 - t * t * (3.0 - 2.0 * t));
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
struct FOut {
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) depth: f32,
|
||||
};
|
||||
|
||||
@fragment
|
||||
fn fs(i: VOut, @builtin(front_facing) front: bool) -> FOut {
|
||||
let mb = i.material * MAT_STRIDE;
|
||||
let m0 = materials[mb];
|
||||
let m1 = materials[mb + 1u];
|
||||
let m2 = materials[mb + 2u];
|
||||
let m3 = materials[mb + 3u];
|
||||
let m4 = materials[mb + 4u];
|
||||
let m5 = materials[mb + 5u];
|
||||
let flags = u32(m4.y);
|
||||
let double_sided = (flags & 1u) != 0u;
|
||||
let unlit = (flags & 2u) != 0u;
|
||||
let accepts_lights = (flags & 4u) != 0u;
|
||||
let receives_shadows = (flags & 8u) != 0u;
|
||||
let alpha_mode = (flags >> 8u) & 3u;
|
||||
if (!front && !double_sided) {
|
||||
discard;
|
||||
}
|
||||
var base = m0;
|
||||
let tex_base = i32(m3.x);
|
||||
if (tex_base >= 0) {
|
||||
base = base * sample_tex(tex_base, i.uv);
|
||||
}
|
||||
let opacity = m5.y;
|
||||
var alpha = base.a * opacity;
|
||||
if (alpha_mode == 1u && alpha < m2.w) {
|
||||
discard;
|
||||
}
|
||||
if (alpha <= 1e-5 && alpha_mode != 0u) {
|
||||
discard;
|
||||
}
|
||||
if (alpha_mode == 0u) {
|
||||
alpha = opacity;
|
||||
}
|
||||
let rgb = base.rgb;
|
||||
var emissive = m1.xyz;
|
||||
let tex_em = i32(m4.x);
|
||||
if (tex_em >= 0) {
|
||||
emissive = emissive * sample_tex(tex_em, i.uv).rgb;
|
||||
}
|
||||
var out: FOut;
|
||||
out.depth = dot(g.view_z.xyz, i.pos) + g.view_z.w;
|
||||
if (unlit || !accepts_lights || g.env2.y < 0.5) {
|
||||
let c = rgb + emissive;
|
||||
out.color = vec4<f32>(c * alpha, alpha);
|
||||
return out;
|
||||
}
|
||||
var metallic = m1.w;
|
||||
var rough = m2.x;
|
||||
let tex_mr = i32(m3.y);
|
||||
if (tex_mr >= 0) {
|
||||
let t = sample_tex(tex_mr, i.uv);
|
||||
rough = rough * t.y;
|
||||
metallic = metallic * t.z;
|
||||
}
|
||||
var n = norm3(i.normal);
|
||||
if (!front) {
|
||||
n = -n;
|
||||
}
|
||||
let tex_n = i32(m3.z);
|
||||
if (tex_n >= 0) {
|
||||
let t = sample_tex(tex_n, i.uv);
|
||||
let ns = m2.y;
|
||||
let tn = vec3<f32>((t.x * 2.0 - 1.0) * ns, (t.y * 2.0 - 1.0) * ns, t.z * 2.0 - 1.0);
|
||||
let tg = norm3(i.tangent.xyz - n * dot(n, i.tangent.xyz));
|
||||
let sign = select(-i.tangent.w, i.tangent.w, front);
|
||||
let bt = cross(n, tg) * sign;
|
||||
n = norm3(tg * tn.x + bt * tn.y + n * tn.z);
|
||||
}
|
||||
var ao = 1.0;
|
||||
let tex_ao = i32(m3.w);
|
||||
if (tex_ao >= 0) {
|
||||
ao = 1.0 + m2.z * (sample_tex(tex_ao, i.uv).x - 1.0);
|
||||
}
|
||||
var v: vec3<f32>;
|
||||
if (g.eye.w > 0.5) {
|
||||
v = -g.fwd.xyz;
|
||||
} else {
|
||||
v = norm3(g.eye.xyz - i.pos);
|
||||
}
|
||||
let diff_k = m4.z;
|
||||
let spec_k = m4.w;
|
||||
let amb_k = m5.x;
|
||||
var acc = vec3<f32>(0.0);
|
||||
let nl_count = u32(g.fwd.w);
|
||||
for (var li = 0u; li < nl_count; li = li + 1u) {
|
||||
let lb = li * LIGHT_STRIDE;
|
||||
let l0 = lights[lb];
|
||||
let l1 = lights[lb + 1u];
|
||||
let l2 = lights[lb + 2u];
|
||||
let l3 = lights[lb + 3u];
|
||||
let l4 = lights[lb + 4u];
|
||||
let kind = u32(l0.w);
|
||||
var to_l: vec3<f32>;
|
||||
var dist: f32;
|
||||
if (kind == 0u) {
|
||||
to_l = -l1.xyz;
|
||||
dist = 1e30;
|
||||
} else {
|
||||
let d = l0.xyz - i.pos;
|
||||
let len = sqrt(dot(d, d));
|
||||
if (len < 1e-6) {
|
||||
to_l = vec3<f32>(0.0, 0.0, -1.0);
|
||||
dist = 0.0;
|
||||
} else {
|
||||
to_l = d / len;
|
||||
dist = len;
|
||||
}
|
||||
}
|
||||
let k = attenuation(kind, u32(l3.x), l3.y, l3.z, l2.w, l1.w, l1.xyz, to_l, dist);
|
||||
if (k <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let n_l = dot(n, to_l);
|
||||
if (n_l <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
var sh = 1.0;
|
||||
if (receives_shadows) {
|
||||
sh = light_shadow(li, kind, l0.xyz, i32(l3.w), l4.x, i.pos, n_l);
|
||||
}
|
||||
let b = brdf(rgb, metallic, rough, spec_k, diff_k, n, v, to_l);
|
||||
acc = acc + b * l2.xyz * k * sh;
|
||||
}
|
||||
acc = acc + g.ambient.xyz * rgb * amb_k * ao;
|
||||
if (g.ambient.w > 0.5) {
|
||||
let n_v = clamp(dot(n, v), 1e-4, 1.0);
|
||||
let r = n * (2.0 * dot(n, v)) - v;
|
||||
let rot = g.env2.x;
|
||||
let irr = sample_tex(i32(g.env.z), equirect_uv(n, rot)).rgb * g.env.w;
|
||||
let mips = u32(g.env.y);
|
||||
let lv = clamp(rough, 0.0, 1.0) * f32(max(mips, 1u) - 1u);
|
||||
let lo = u32(floor(lv));
|
||||
let hi = min(lo + 1u, max(mips, 1u) - 1u);
|
||||
let f = lv - f32(lo);
|
||||
let ruv = equirect_uv(r, rot);
|
||||
let sa = sample_tex(i32(g.env.x) + i32(lo), ruv).rgb;
|
||||
let sb = sample_tex(i32(g.env.x) + i32(hi), ruv).rgb;
|
||||
let spec = (sa + (sb - sa) * f) * g.env.w;
|
||||
let f0 = mix(vec3<f32>(0.04 * spec_k), rgb, metallic);
|
||||
let ab = env_brdf(n_v, rough);
|
||||
let fs = f0 * ab.x + vec3<f32>(ab.y);
|
||||
acc = acc + (irr * rgb * (vec3<f32>(1.0) - fs) * (1.0 - metallic) * diff_k + spec * fs) * ao;
|
||||
}
|
||||
let c = acc + emissive;
|
||||
out.color = vec4<f32>(c * alpha, alpha);
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Advanced 3D: the GPU rasteriser against the CPU reference on the same scenes.
|
||||
//!
|
||||
//! Tolerances: the GPU's colour target is half float (relative error ≤ 1e-3) and its
|
||||
//! rasteriser and interpolators differ from the CPU's in the last bits, so pixels on triangle
|
||||
//! edges may be covered by the other neighbour. Interior pixels agree within 2/255; at most
|
||||
//! 1.5 % of pixels (silhouettes) may differ more. Skipped without an adapter.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, LightKind, PrimitiveKind, Project, Renderer as R3, Solid, build};
|
||||
use effectcraft_render::three_d::adv::{self, Scene};
|
||||
use effectcraft_render::{Accelerator, EvalCtx, NoFootage, RenderOpts, Renderer};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::Gpu;
|
||||
|
||||
fn gpu() -> Option<Gpu> {
|
||||
let g = Gpu::headless();
|
||||
if g.is_none() {
|
||||
eprintln!("effectcraft-gpu adv3d tests: no GPU adapter, skipping");
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn add(p: &mut Project, cid: ItemId, src: LayerSource, edit: impl FnOnce(&mut effectcraft_project::Layer)) {
|
||||
let comp = p.comp(cid).unwrap().clone();
|
||||
let mut l = build::layer(p, &comp, "L", src, (comp.width, comp.height), None);
|
||||
edit(&mut l);
|
||||
p.comp_mut(cid).unwrap().layers.insert(0, l);
|
||||
}
|
||||
|
||||
fn set(l: &mut effectcraft_project::Layer, path: &str, v: Value) {
|
||||
l.props.prop_mut(path).unwrap_or_else(|| panic!("{path}")).value = v;
|
||||
}
|
||||
|
||||
/// Primitives with metal/dielectric materials, a shadow-casting spot light and a point light,
|
||||
/// an environment light, a semi-transparent textured card.
|
||||
fn scene_project() -> (Project, ItemId) {
|
||||
let mut p = Project::default();
|
||||
let mut c = Comp::new(240, 160, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
|
||||
c.renderer = R3::Advanced3D;
|
||||
let cid = p.add_item("C", Default::default(), None, ItemKind::Comp(c.into()));
|
||||
add(&mut p, cid, LayerSource::Primitive { kind: PrimitiveKind::Plane }, |l| {
|
||||
set(l, "geometryOptions/width", Value::Scalar(900.0));
|
||||
set(l, "geometryOptions/height", Value::Scalar(900.0));
|
||||
set(l, "transform/position", Value::Vec3([120.0, 130.0, 0.0]));
|
||||
set(l, "transform/rotationX", Value::Scalar(90.0));
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Primitive { kind: PrimitiveKind::Sphere }, |l| {
|
||||
set(l, "geometryOptions/radius", Value::Scalar(35.0));
|
||||
set(l, "transform/position", Value::Vec3([80.0, 95.0, 0.0]));
|
||||
set(l, "materialOptions/baseColor", Value::Color([0.9, 0.2, 0.2, 1.0]));
|
||||
set(l, "materialOptions/roughness", Value::Scalar(35.0));
|
||||
set(l, "materialOptions/castsShadows", Value::Enum(1));
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Primitive { kind: PrimitiveKind::Torus }, |l| {
|
||||
set(l, "geometryOptions/radius", Value::Scalar(35.0));
|
||||
set(l, "geometryOptions/tubeRadius", Value::Scalar(12.0));
|
||||
set(l, "transform/position", Value::Vec3([165.0, 90.0, 0.0]));
|
||||
set(l, "transform/rotationX", Value::Scalar(60.0));
|
||||
set(l, "materialOptions/metallic", Value::Scalar(100.0));
|
||||
set(l, "materialOptions/roughness", Value::Scalar(20.0));
|
||||
set(l, "materialOptions/castsShadows", Value::Enum(1));
|
||||
});
|
||||
let sid = p.add_item("S", Default::default(), None, ItemKind::Solid(Solid { color: [0.2, 0.6, 0.9], width: 60, height: 40, pixel_aspect: 1.0 }));
|
||||
add(&mut p, cid, LayerSource::Solid { item: sid }, |l| {
|
||||
l.switches.three_d = true;
|
||||
set(l, "transform/opacity", Value::Scalar(60.0));
|
||||
set(l, "transform/position", Value::Vec3([120.0, 80.0, -60.0]));
|
||||
set(l, "transform/rotationY", Value::Scalar(30.0));
|
||||
});
|
||||
let env = p.add_item("E", Default::default(), None, ItemKind::Solid(Solid { color: [0.6, 0.7, 1.0], width: 32, height: 16, pixel_aspect: 1.0 }));
|
||||
add(&mut p, cid, LayerSource::Solid { item: env }, |l| {
|
||||
l.environment = true;
|
||||
l.switches.three_d = true;
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Environment }, |l| set(l, "lightOptions/intensity", Value::Scalar(50.0)));
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Spot }, |l| {
|
||||
set(l, "transform/position", Value::Vec3([40.0, -120.0, -200.0]));
|
||||
set(l, "transform/poi", Value::Vec3([120.0, 100.0, 0.0]));
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
set(l, "lightOptions/coneAngle", Value::Scalar(110.0));
|
||||
set(l, "lightOptions/shadowDiffusion", Value::Scalar(4.0));
|
||||
});
|
||||
add(&mut p, cid, LayerSource::Light { kind: LightKind::Point }, |l| {
|
||||
set(l, "transform/position", Value::Vec3([200.0, 20.0, -100.0]));
|
||||
set(l, "lightOptions/intensity", Value::Scalar(40.0));
|
||||
set(l, "lightOptions/falloff", Value::Enum(1));
|
||||
});
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
fn scene(p: &Project, cid: ItemId) -> Scene {
|
||||
let r = Renderer::new(p, &NoFootage, RenderOpts::default());
|
||||
let ctx = EvalCtx { project: p, comp_id: cid, comp: p.comp(cid).unwrap(), time: Tick::ZERO, expr: None };
|
||||
let run: Vec<&effectcraft_project::Layer> = ctx.comp.layers.iter().rev().filter(|l| l.is_3d() && l.has_video()).collect();
|
||||
adv::scene_of(&r, &ctx, &run, (ctx.comp.width, ctx.comp.height))
|
||||
}
|
||||
|
||||
fn compare(s: &Scene, g: &Gpu) {
|
||||
let cpu = adv::raster::render(s);
|
||||
let gt = g.raster_3d(s).expect("gpu raster");
|
||||
assert_eq!((gt.width, gt.height), (cpu.width, cpu.height));
|
||||
let n = cpu.color.len();
|
||||
let mut bad = 0;
|
||||
let mut sum = 0.0f64;
|
||||
for (a, b) in cpu.color.iter().zip(>.color) {
|
||||
let d = (0..4).map(|k| (a[k] - b[k]).abs() / a[k].abs().max(1.0)).fold(0.0f32, f32::max);
|
||||
sum += d as f64;
|
||||
if d > 2.0 / 255.0 {
|
||||
bad += 1;
|
||||
}
|
||||
}
|
||||
let frac = bad as f64 / n as f64;
|
||||
eprintln!("adv3d gpu vs cpu: {bad}/{n} pixels beyond 2/255 ({:.3} %), mean {:.5}", frac * 100.0, sum / n as f64);
|
||||
assert!(frac < 0.015, "{:.3} % of pixels differ", frac * 100.0);
|
||||
assert!(sum / (n as f64) < 0.002);
|
||||
// Depth agrees where both see a surface.
|
||||
let mut dz = 0;
|
||||
for (a, b) in cpu.depth.iter().zip(>.depth) {
|
||||
if a.is_finite() != b.is_finite() || (a.is_finite() && (a - b).abs() > 0.01 * a.abs().max(1.0)) {
|
||||
dz += 1;
|
||||
}
|
||||
}
|
||||
assert!((dz as f64) < 0.015 * n as f64, "{dz} depth mismatches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_matches_cpu_lit_scene() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let (p, cid) = scene_project();
|
||||
let s = scene(&p, cid);
|
||||
assert!(!s.shadows.is_empty() && s.env.is_some() && s.opaque_count < s.indices.len() as u32);
|
||||
compare(&s, &g);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_matches_cpu_unlit_and_full_frames() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let (mut p, cid) = scene_project();
|
||||
// Unlit: remove the lights.
|
||||
p.comp_mut(cid).unwrap().layers.retain(|l| !l.is_light());
|
||||
compare(&scene(&p, cid), &g);
|
||||
// Whole frames through the renderer with the GPU attached.
|
||||
let (p, cid) = scene_project();
|
||||
let cpu = Renderer::new(&p, &NoFootage, RenderOpts::default()).comp_frame(cid, Tick::ZERO);
|
||||
let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() });
|
||||
r.accel = Some(&g);
|
||||
let gimg = r.comp_frame(cid, Tick::ZERO);
|
||||
let diff = cpu.data.iter().zip(&gimg.data).filter(|(a, b)| (0..4).any(|k| (a[k] - b[k]).abs() > 3.0 / 255.0)).count();
|
||||
assert!((diff as f64) < 0.02 * cpu.data.len() as f64, "{diff} pixels differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_raster_perf_smoke() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let (p, cid) = scene_project();
|
||||
let s = scene(&p, cid);
|
||||
let t0 = std::time::Instant::now();
|
||||
let _ = adv::raster::render(&s);
|
||||
let cpu = t0.elapsed();
|
||||
// First use compiles the pipelines.
|
||||
let _ = g.raster_3d(&s);
|
||||
let t0 = std::time::Instant::now();
|
||||
let _ = g.raster_3d(&s);
|
||||
let gpu = t0.elapsed();
|
||||
eprintln!("adv3d 480x320 (2x SSAA of 240x160), {} triangles: cpu {:?}, gpu {:?}", s.indices.len() / 3, cpu, gpu);
|
||||
}
|
||||
Reference in New Issue
Block a user