Complete Bevy OpenSim scene rendering
This commit is contained in:
@@ -9,11 +9,13 @@ description = "Reusable headless wgpu scene renderer and projection camera"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
wgpu = { version = "30.0.1", default-features = false, features = ["std", "dx12", "metal", "gles", "vulkan", "wgsl"], optional = true }
|
||||
bevy = { version = "0.19.1", default-features = false, features = ["3d_bevy_render", "default_app", "multi_threaded", "pbr_specular_textures"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wgpu = ["dep:wgpu"]
|
||||
bevy-engine = ["dep:bevy"]
|
||||
wgpu = ["bevy-engine"]
|
||||
windowed = ["bevy-engine", "bevy/bevy_winit", "bevy/wayland", "bevy/x11"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
1100
crates/metacrate-rendering-wgpu/src/gpu.rs
Normal file
1100
crates/metacrate-rendering-wgpu/src/gpu.rs
Normal file
File diff suppressed because it is too large
Load Diff
64
crates/metacrate-rendering-wgpu/src/legacy_material.wgsl
Normal file
64
crates/metacrate-rendering-wgpu/src/legacy_material.wgsl
Normal file
@@ -0,0 +1,64 @@
|
||||
#import bevy_pbr::forward_io::{VertexOutput, FragmentOutput}
|
||||
|
||||
struct LegacyUniform {
|
||||
diffuse_color: vec4<f32>,
|
||||
specular_color: vec4<f32>,
|
||||
diffuse_scale_offset: vec4<f32>,
|
||||
normal_scale_offset: vec4<f32>,
|
||||
specular_scale_offset: vec4<f32>,
|
||||
rotations: vec4<f32>,
|
||||
surface: vec4<f32>,
|
||||
camera_position: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material: LegacyUniform;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var normal_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var normal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var specular_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var specular_sampler: sampler;
|
||||
|
||||
fn transformed_uv(uv: vec2<f32>, scale_offset: vec4<f32>, rotation: f32) -> vec2<f32> {
|
||||
let centered = (uv - vec2(0.5)) * scale_offset.xy;
|
||||
let sine = sin(rotation);
|
||||
let cosine = cos(rotation);
|
||||
return vec2(
|
||||
centered.x * cosine - centered.y * sine,
|
||||
centered.x * sine + centered.y * cosine,
|
||||
) + vec2(0.5) + scale_offset.zw;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
|
||||
let diffuse_uv = transformed_uv(in.uv, material.diffuse_scale_offset, material.rotations.x);
|
||||
let texel = textureSample(diffuse_texture, diffuse_sampler, diffuse_uv);
|
||||
var base = material.diffuse_color * texel * in.color;
|
||||
if material.surface.w >= 0.0 && base.a < material.surface.w {
|
||||
discard;
|
||||
}
|
||||
|
||||
var normal = normalize(in.world_normal) * select(-1.0, 1.0, is_front);
|
||||
if material.surface.z > 0.5 {
|
||||
let normal_uv = transformed_uv(in.uv, material.normal_scale_offset, material.rotations.y);
|
||||
let tangent_normal = textureSample(normal_texture, normal_sampler, normal_uv).xyz * 2.0 - 1.0;
|
||||
let tangent = normalize(in.world_tangent.xyz);
|
||||
let bitangent = normalize(cross(normal, tangent)) * in.world_tangent.w;
|
||||
normal = normalize(mat3x3(tangent, bitangent, normal) * tangent_normal);
|
||||
}
|
||||
|
||||
let light_direction = normalize(vec3(0.35, 0.82, 0.45));
|
||||
let view_direction = normalize(material.camera_position.xyz - in.world_position.xyz);
|
||||
let half_direction = normalize(light_direction + view_direction);
|
||||
let diffuse_light = max(dot(normal, light_direction), 0.0);
|
||||
let specular_uv = transformed_uv(in.uv, material.specular_scale_offset, material.rotations.z);
|
||||
let specular_map = textureSample(specular_texture, specular_sampler, specular_uv).rgb;
|
||||
let specular_light = pow(max(dot(normal, half_direction), 0.0), material.rotations.w);
|
||||
let lit = base.rgb * (0.55 + 0.45 * diffuse_light)
|
||||
+ material.specular_color.rgb * specular_map * specular_light
|
||||
+ material.specular_color.rgb * material.surface.x * 0.15;
|
||||
|
||||
var out: FragmentOutput;
|
||||
out.color = vec4(select(lit, base.rgb, material.surface.y > 0.5), base.a);
|
||||
return out;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Cross-platform projection camera and headless `wgpu` triangle renderer.
|
||||
//! Reusable, windowless `OpenSim` scene rendering on stable `wgpu`.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
@@ -19,14 +19,9 @@ impl Camera {
|
||||
up: [f32; 3],
|
||||
vertical_fov_degrees: f32,
|
||||
) -> Result<Self, RenderError> {
|
||||
let forward = normalize([
|
||||
target[0] - position[0],
|
||||
target[1] - position[1],
|
||||
target[2] - position[2],
|
||||
])?;
|
||||
let camera = Self {
|
||||
position,
|
||||
forward,
|
||||
forward: normalize(sub(target, position))?,
|
||||
up: normalize(up)?,
|
||||
vertical_fov_degrees,
|
||||
};
|
||||
@@ -51,10 +46,149 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 3],
|
||||
pub normal: [f32; 3],
|
||||
pub tex_coord: [f32; 2],
|
||||
pub color_srgb: [u8; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Mesh {
|
||||
pub vertices: Vec<Vertex>,
|
||||
pub indices: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Triangle {
|
||||
pub vertices: [[f32; 3]; 3],
|
||||
pub colors_srgb: [[u8; 4]; 3],
|
||||
pub struct Texture {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct UvTransform {
|
||||
pub scale: [f32; 2],
|
||||
pub offset: [f32; 2],
|
||||
pub rotation_radians: f32,
|
||||
}
|
||||
|
||||
impl Default for UvTransform {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: [1.0; 2],
|
||||
offset: [0.0; 2],
|
||||
rotation_radians: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct TextureSlot {
|
||||
pub texture: Option<usize>,
|
||||
pub transform: UvTransform,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum AlphaMode {
|
||||
#[default]
|
||||
Opaque,
|
||||
Mask {
|
||||
cutoff: f32,
|
||||
},
|
||||
Blend,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BlinnPhongMaterial {
|
||||
pub diffuse_color_srgb: [u8; 4],
|
||||
pub diffuse: TextureSlot,
|
||||
pub normal: TextureSlot,
|
||||
pub specular: TextureSlot,
|
||||
pub specular_color_srgb: [u8; 3],
|
||||
pub shininess: f32,
|
||||
pub environment_intensity: f32,
|
||||
pub fullbright: bool,
|
||||
pub double_sided: bool,
|
||||
pub alpha_mode: AlphaMode,
|
||||
}
|
||||
|
||||
impl Default for BlinnPhongMaterial {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
diffuse_color_srgb: [255; 4],
|
||||
diffuse: TextureSlot::default(),
|
||||
normal: TextureSlot::default(),
|
||||
specular: TextureSlot::default(),
|
||||
specular_color_srgb: [0; 3],
|
||||
shininess: 32.0,
|
||||
environment_intensity: 0.0,
|
||||
fullbright: false,
|
||||
double_sided: false,
|
||||
alpha_mode: AlphaMode::Opaque,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PbrMaterial {
|
||||
pub base_color_srgb: [u8; 4],
|
||||
pub base_color: TextureSlot,
|
||||
pub normal: TextureSlot,
|
||||
/// glTF convention: roughness in G and metallic in B.
|
||||
pub metallic_roughness: TextureSlot,
|
||||
pub emissive: TextureSlot,
|
||||
pub metallic_factor: f32,
|
||||
pub roughness_factor: f32,
|
||||
pub emissive_factor_srgb: [u8; 3],
|
||||
pub double_sided: bool,
|
||||
pub alpha_mode: AlphaMode,
|
||||
}
|
||||
|
||||
impl Default for PbrMaterial {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_color_srgb: [255; 4],
|
||||
base_color: TextureSlot::default(),
|
||||
normal: TextureSlot::default(),
|
||||
metallic_roughness: TextureSlot::default(),
|
||||
emissive: TextureSlot::default(),
|
||||
metallic_factor: 1.0,
|
||||
roughness_factor: 1.0,
|
||||
emissive_factor_srgb: [0; 3],
|
||||
double_sided: false,
|
||||
alpha_mode: AlphaMode::Opaque,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Material {
|
||||
BlinnPhong(BlinnPhongMaterial),
|
||||
Pbr(PbrMaterial),
|
||||
}
|
||||
|
||||
impl Default for Material {
|
||||
fn default() -> Self {
|
||||
Self::BlinnPhong(BlinnPhongMaterial::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Material {
|
||||
#[must_use]
|
||||
pub fn alpha_mode(&self) -> AlphaMode {
|
||||
match self {
|
||||
Self::BlinnPhong(material) => material.alpha_mode,
|
||||
Self::Pbr(material) => material.alpha_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Renderable {
|
||||
pub mesh: Mesh,
|
||||
pub material: Material,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -62,16 +196,19 @@ pub struct RenderLimits {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub max_triangles: usize,
|
||||
pub max_texture_bytes: usize,
|
||||
pub max_texture_pixels: usize,
|
||||
pub far_distance: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RenderError {
|
||||
AdapterUnavailable,
|
||||
InvalidScene,
|
||||
ResourceLimit,
|
||||
TimedOut,
|
||||
Readback,
|
||||
Device(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RenderError {
|
||||
@@ -82,18 +219,27 @@ impl std::fmt::Display for RenderError {
|
||||
Self::ResourceLimit => "render resource limit exceeded",
|
||||
Self::TimedOut => "wgpu render timed out",
|
||||
Self::Readback => "wgpu readback failed",
|
||||
})
|
||||
Self::Device(_) => "wgpu device failed",
|
||||
})?;
|
||||
if let Self::Device(detail) = self {
|
||||
write!(formatter, ": {detail}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RenderError {}
|
||||
|
||||
fn sub(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||
std::array::from_fn(|axis| left[axis] - right[axis])
|
||||
}
|
||||
|
||||
fn normalize(value: [f32; 3]) -> Result<[f32; 3], RenderError> {
|
||||
let length = dot(value, value).sqrt();
|
||||
if length < 0.0001 {
|
||||
if length < 0.0001 || !length.is_finite() {
|
||||
return Err(RenderError::InvalidScene);
|
||||
}
|
||||
Ok([value[0] / length, value[1] / length, value[2] / length])
|
||||
Ok(value.map(|component| component / length))
|
||||
}
|
||||
|
||||
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||
@@ -109,318 +255,30 @@ fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub struct Renderer {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
mod gpu;
|
||||
#[cfg(feature = "wgpu")]
|
||||
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] =
|
||||
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x4];
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl Renderer {
|
||||
pub async fn new() -> Result<Self, RenderError> {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::LowPower,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|_| RenderError::AdapterUnavailable)?;
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_defaults(),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|_| RenderError::AdapterUnavailable)?;
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
r"
|
||||
struct VertexInput { @location(0) position: vec3<f32>, @location(1) color: vec4<f32>, };
|
||||
struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) color: vec4<f32>, };
|
||||
@vertex fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
output.position = vec4<f32>(input.position, 1.0);
|
||||
output.color = input.color;
|
||||
return output;
|
||||
}
|
||||
@fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return input.color; }
|
||||
"
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-layout"),
|
||||
bind_group_layouts: &[],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 7 * std::mem::size_of::<f32>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &VERTEX_ATTRIBUTES,
|
||||
})],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
Ok(Self {
|
||||
device,
|
||||
queue,
|
||||
pipeline,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn render(
|
||||
&self,
|
||||
camera: Camera,
|
||||
triangles: &[Triangle],
|
||||
background_srgb: [u8; 4],
|
||||
limits: RenderLimits,
|
||||
) -> Result<Vec<u8>, RenderError> {
|
||||
let (vertices, vertex_count) = gpu_vertices(camera, triangles, limits)?;
|
||||
let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-vertices"),
|
||||
size: vertices.len().max(4) as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
if !vertices.is_empty() {
|
||||
self.queue.write_buffer(&vertex_buffer, 0, &vertices);
|
||||
}
|
||||
let extent = wgpu::Extent3d {
|
||||
width: limits.width,
|
||||
height: limits.height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
let color = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-color"),
|
||||
size: extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let depth = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-depth"),
|
||||
size: extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let row_bytes = limits
|
||||
.width
|
||||
.checked_mul(4)
|
||||
.ok_or(RenderError::ResourceLimit)?;
|
||||
let padded_row_bytes = row_bytes.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
|
||||
* wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-readback"),
|
||||
size: u64::from(padded_row_bytes)
|
||||
.checked_mul(u64::from(limits.height))
|
||||
.ok_or(RenderError::ResourceLimit)?,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let color_view = color.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-commands"),
|
||||
});
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("metacrate-rendering-wgpu-pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &color_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: f64::from(background_srgb[0]) / 255.0,
|
||||
g: f64::from(background_srgb[1]) / 255.0,
|
||||
b: f64::from(background_srgb[2]) / 255.0,
|
||||
a: f64::from(background_srgb[3]) / 255.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
if vertex_count != 0 {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
|
||||
pass.draw(0..vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &color,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &readback,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padded_row_bytes),
|
||||
rows_per_image: Some(limits.height),
|
||||
},
|
||||
},
|
||||
extent,
|
||||
);
|
||||
let submission = self.queue.submit([encoder.finish()]);
|
||||
let slice = readback.slice(..);
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
self.device
|
||||
.poll(wgpu::PollType::Wait {
|
||||
submission_index: Some(submission),
|
||||
timeout: Some(std::time::Duration::from_secs(5)),
|
||||
})
|
||||
.map_err(|_| RenderError::TimedOut)?;
|
||||
receiver
|
||||
.recv_timeout(std::time::Duration::from_secs(1))
|
||||
.map_err(|_| RenderError::TimedOut)?
|
||||
.map_err(|_| RenderError::Readback)?;
|
||||
let mapped = slice
|
||||
.get_mapped_range()
|
||||
.map_err(|_| RenderError::Readback)?;
|
||||
let mut rgba = Vec::with_capacity(row_bytes as usize * limits.height as usize);
|
||||
for row in mapped
|
||||
.chunks_exact(padded_row_bytes as usize)
|
||||
.take(limits.height as usize)
|
||||
{
|
||||
rgba.extend_from_slice(&row[..row_bytes as usize]);
|
||||
}
|
||||
drop(mapped);
|
||||
readback.unmap();
|
||||
Ok(rgba)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[allow(clippy::cast_precision_loss)] // Render dimensions and distance are bounded well below f32's exact integer range.
|
||||
fn gpu_vertices(
|
||||
camera: Camera,
|
||||
triangles: &[Triangle],
|
||||
limits: RenderLimits,
|
||||
) -> Result<(Vec<u8>, u32), RenderError> {
|
||||
if limits.width == 0
|
||||
|| limits.height == 0
|
||||
|| limits.far_distance == 0
|
||||
|| triangles.len() > limits.max_triangles
|
||||
|| triangles
|
||||
.iter()
|
||||
.flat_map(|triangle| triangle.vertices.iter().flatten())
|
||||
.any(|value| !value.is_finite())
|
||||
{
|
||||
return Err(RenderError::InvalidScene);
|
||||
}
|
||||
let basis = camera.basis()?;
|
||||
let aspect = limits.width as f32 / limits.height as f32;
|
||||
let focal = 1.0 / (camera.vertical_fov_degrees.to_radians() / 2.0).tan();
|
||||
let mut bytes = Vec::with_capacity(triangles.len() * 3 * 7 * std::mem::size_of::<f32>());
|
||||
let mut count = 0u32;
|
||||
for triangle in triangles {
|
||||
let projected = triangle.vertices.map(|world| {
|
||||
let delta = [
|
||||
world[0] - camera.position[0],
|
||||
world[1] - camera.position[1],
|
||||
world[2] - camera.position[2],
|
||||
];
|
||||
let z = dot(delta, basis.2);
|
||||
[
|
||||
dot(delta, basis.0) * focal / aspect / z,
|
||||
dot(delta, basis.1) * focal / z,
|
||||
(z / limits.far_distance as f32).clamp(0.0, 1.0),
|
||||
z,
|
||||
]
|
||||
});
|
||||
if projected.iter().any(|vertex| vertex[3] <= 0.05) {
|
||||
continue;
|
||||
}
|
||||
for (vertex, color) in projected.into_iter().zip(triangle.colors_srgb) {
|
||||
let color = color.map(|channel| f32::from(channel) / 255.0);
|
||||
for value in vertex[..3].iter().chain(color.iter()) {
|
||||
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
count = count.checked_add(1).ok_or(RenderError::ResourceLimit)?;
|
||||
}
|
||||
}
|
||||
Ok((bytes, count))
|
||||
}
|
||||
pub use gpu::Renderer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn look_at_builds_a_valid_projection_camera() {
|
||||
fn camera_and_both_material_models_are_explicit() {
|
||||
let camera =
|
||||
Camera::look_at([1.0, 2.0, 3.0], [5.0, 2.0, 3.0], [0.0, 0.0, 1.0], 60.0).unwrap();
|
||||
assert!(
|
||||
camera
|
||||
.forward
|
||||
.into_iter()
|
||||
.iter()
|
||||
.zip([1.0, 0.0, 0.0])
|
||||
.all(|(actual, expected)| (actual - expected).abs() < f32::EPSILON)
|
||||
);
|
||||
assert!(Camera::look_at([0.0; 3], [0.0; 3], [0.0, 0.0, 1.0], 60.0).is_err());
|
||||
assert!(matches!(Material::default(), Material::BlinnPhong(_)));
|
||||
assert!(matches!(
|
||||
Material::Pbr(PbrMaterial::default()),
|
||||
Material::Pbr(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
85
crates/metacrate-rendering-wgpu/src/pbr_material.wgsl
Normal file
85
crates/metacrate-rendering-wgpu/src/pbr_material.wgsl
Normal file
@@ -0,0 +1,85 @@
|
||||
#import bevy_pbr::{
|
||||
forward_io::{VertexOutput, FragmentOutput},
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
pbr_functions::{alpha_discard, apply_normal_mapping, apply_pbr_lighting, calculate_tbn_mikktspace, main_pass_post_lighting_processing},
|
||||
pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT,
|
||||
}
|
||||
|
||||
struct PbrExtension {
|
||||
base_scale_offset: vec4<f32>,
|
||||
normal_scale_offset: vec4<f32>,
|
||||
metallic_roughness_scale_offset: vec4<f32>,
|
||||
emissive_scale_offset: vec4<f32>,
|
||||
rotations: vec4<f32>,
|
||||
maps: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var<uniform> extension: PbrExtension;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var base_color_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(102) var base_color_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var normal_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var normal_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(105) var metallic_roughness_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(106) var metallic_roughness_sampler: sampler;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(107) var emissive_texture: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(108) var emissive_sampler: sampler;
|
||||
|
||||
fn transformed_uv(uv: vec2<f32>, scale_offset: vec4<f32>, rotation: f32) -> vec2<f32> {
|
||||
let centered = (uv - vec2(0.5)) * scale_offset.xy;
|
||||
let sine = sin(rotation);
|
||||
let cosine = cos(rotation);
|
||||
return vec2(
|
||||
centered.x * cosine - centered.y * sine,
|
||||
centered.x * sine + centered.y * cosine,
|
||||
) + vec2(0.5) + scale_offset.zw;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
|
||||
if extension.maps.x > 0.5 {
|
||||
let uv = transformed_uv(in.uv, extension.base_scale_offset, extension.rotations.x);
|
||||
pbr_input.material.base_color *= textureSample(base_color_texture, base_color_sampler, uv);
|
||||
}
|
||||
if extension.maps.y > 0.5 {
|
||||
let uv = transformed_uv(in.uv, extension.normal_scale_offset, extension.rotations.y);
|
||||
let sampled_normal = textureSample(normal_texture, normal_sampler, uv).rgb;
|
||||
let tbn = calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent);
|
||||
let double_sided = (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u;
|
||||
pbr_input.N = apply_normal_mapping(
|
||||
pbr_input.material.flags,
|
||||
tbn,
|
||||
double_sided,
|
||||
is_front,
|
||||
sampled_normal,
|
||||
);
|
||||
}
|
||||
if extension.maps.z > 0.5 {
|
||||
let uv = transformed_uv(
|
||||
in.uv,
|
||||
extension.metallic_roughness_scale_offset,
|
||||
extension.rotations.z,
|
||||
);
|
||||
let metallic_roughness = textureSample(
|
||||
metallic_roughness_texture,
|
||||
metallic_roughness_sampler,
|
||||
uv,
|
||||
);
|
||||
pbr_input.material.perceptual_roughness *= metallic_roughness.g;
|
||||
pbr_input.material.metallic *= metallic_roughness.b;
|
||||
}
|
||||
if extension.maps.w > 0.5 {
|
||||
let uv = transformed_uv(in.uv, extension.emissive_scale_offset, extension.rotations.w);
|
||||
pbr_input.material.emissive *= textureSample(emissive_texture, emissive_sampler, uv);
|
||||
}
|
||||
|
||||
pbr_input.material.base_color = alpha_discard(
|
||||
pbr_input.material,
|
||||
pbr_input.material.base_color,
|
||||
);
|
||||
var out: FragmentOutput;
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user