Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
//! [`ForwardRenderer`]: a single-pass forward renderer with a depth buffer and
|
||||
//! one directional light.
|
||||
//!
|
||||
//! Stage 4 scope: draw a list of [`RenderObject`]s (each a [`GpuMesh`] +
|
||||
//! [`Material`] + [`Transform`]) through the lit shader, into a caller-provided
|
||||
//! color target, using an owned depth texture. Shadows, multiple lights, and
|
||||
//! post-processing arrive in later stages.
|
||||
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::Mat3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::mesh::{GpuMesh, Vertex};
|
||||
use super::{Camera, Material};
|
||||
use crate::math::{Color, Transform, Vec3, Vec4};
|
||||
|
||||
/// Depth buffer format used by the forward pass.
|
||||
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
|
||||
/// A directional light: parallel rays with a travel `direction`.
|
||||
///
|
||||
/// Also a **reflected, addable component**: drop one on an entity to author a
|
||||
/// sun/key light in the scene, dual-editable from the editor and scripts.
|
||||
/// (Gathering light entities into the forward pass is a later-stage wiring; the
|
||||
/// renderer currently takes its [`Lighting`] directly.)
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, crate::reflect::Reflect)]
|
||||
pub struct DirectionalLight {
|
||||
/// The direction the light travels (does not need to be normalized).
|
||||
pub direction: Vec3,
|
||||
/// Light color.
|
||||
pub color: Color,
|
||||
/// Scalar intensity multiplier.
|
||||
pub intensity: f32,
|
||||
}
|
||||
|
||||
impl Default for DirectionalLight {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
direction: Vec3::new(-0.5, -1.0, -0.35),
|
||||
color: Color::WHITE,
|
||||
intensity: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scene lighting for a forward pass: one directional light plus an ambient term.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Lighting {
|
||||
/// The single directional (sun) light.
|
||||
pub light: DirectionalLight,
|
||||
/// Flat ambient color added everywhere (cheap fill light).
|
||||
pub ambient: Color,
|
||||
}
|
||||
|
||||
impl Default for Lighting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
light: DirectionalLight::default(),
|
||||
ambient: Color::rgb(0.08, 0.08, 0.10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One drawable: a GPU mesh placed by `transform` and shaded with `material`.
|
||||
pub struct RenderObject<'a> {
|
||||
/// The mesh to draw.
|
||||
pub mesh: &'a GpuMesh,
|
||||
/// Its surface material.
|
||||
pub material: Material,
|
||||
/// World placement.
|
||||
pub transform: Transform,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct GlobalsUniform {
|
||||
view_proj: [[f32; 4]; 4],
|
||||
camera_pos: [f32; 4],
|
||||
light_dir: [f32; 4],
|
||||
light_color: [f32; 4],
|
||||
ambient: [f32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct ObjectUniform {
|
||||
model: [[f32; 4]; 4],
|
||||
normal_mtx: [[f32; 4]; 4],
|
||||
albedo: [f32; 4],
|
||||
mr: [f32; 4],
|
||||
}
|
||||
|
||||
/// A forward renderer owning its pipeline, depth buffer, and uniform storage.
|
||||
pub struct ForwardRenderer {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
globals_buffer: wgpu::Buffer,
|
||||
globals_bind_group: wgpu::BindGroup,
|
||||
object_layout: wgpu::BindGroupLayout,
|
||||
object_buffer: wgpu::Buffer,
|
||||
object_bind_group: wgpu::BindGroup,
|
||||
/// Per-object stride: `size_of::<ObjectUniform>` rounded up to the device's
|
||||
/// minimum dynamic-uniform-buffer offset alignment.
|
||||
object_stride: u64,
|
||||
object_capacity: u32,
|
||||
depth: Option<DepthTarget>,
|
||||
color_format: wgpu::TextureFormat,
|
||||
}
|
||||
|
||||
struct DepthTarget {
|
||||
view: wgpu::TextureView,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ForwardRenderer {
|
||||
/// Builds the renderer for a given color target format (e.g. the surface
|
||||
/// format for a window, or `Rgba8Unorm` for offscreen rendering).
|
||||
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("oxide.forward.lit"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/lit.wgsl").into()),
|
||||
});
|
||||
|
||||
let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("oxide.forward.globals_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: NonZeroU64::new(std::mem::size_of::<GlobalsUniform>() as u64),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let object_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("oxide.forward.object_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: true,
|
||||
min_binding_size: NonZeroU64::new(std::mem::size_of::<ObjectUniform>() as u64),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("oxide.forward.pipeline_layout"),
|
||||
bind_group_layouts: &[Some(&globals_layout), Some(&object_layout)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("oxide.forward.pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[Vertex::LAYOUT],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
unclipped_depth: false,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: DEPTH_FORMAT,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("oxide.forward.globals"),
|
||||
size: std::mem::size_of::<GlobalsUniform>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("oxide.forward.globals_bg"),
|
||||
layout: &globals_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: globals_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
let object_stride = align_up(
|
||||
std::mem::size_of::<ObjectUniform>() as u64,
|
||||
device.limits().min_uniform_buffer_offset_alignment as u64,
|
||||
);
|
||||
let object_capacity = 16;
|
||||
let (object_buffer, object_bind_group) =
|
||||
create_object_storage(device, &object_layout, object_stride, object_capacity);
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
globals_buffer,
|
||||
globals_bind_group,
|
||||
object_layout,
|
||||
object_buffer,
|
||||
object_bind_group,
|
||||
object_stride,
|
||||
object_capacity,
|
||||
depth: None,
|
||||
color_format,
|
||||
}
|
||||
}
|
||||
|
||||
/// The color target format this renderer was built for.
|
||||
pub fn color_format(&self) -> wgpu::TextureFormat {
|
||||
self.color_format
|
||||
}
|
||||
|
||||
/// Renders `objects` into `target` (whose full physical size is
|
||||
/// `width`×`height`) as seen by `camera` placed at `view_transform`, lit
|
||||
/// by `lighting`. Drawing is restricted to `viewport_rect` (a sub-
|
||||
/// rectangle of the target), and the projection uses that rect's aspect
|
||||
/// ratio.
|
||||
///
|
||||
/// The color target is *loaded* (not cleared) so a clear pass run before
|
||||
/// this — e.g. the window's clear color — shows through as the background;
|
||||
/// the depth buffer is cleared to 1.0 each call.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
target: &wgpu::TextureView,
|
||||
(width, height): (u32, u32),
|
||||
viewport_rect: crate::math::Rect,
|
||||
camera: &Camera,
|
||||
view_transform: &Transform,
|
||||
lighting: &Lighting,
|
||||
objects: &[RenderObject<'_>],
|
||||
) {
|
||||
let (width, height) = (width.max(1), height.max(1));
|
||||
// Clamp the viewport rect to the target so wgpu doesn't complain.
|
||||
let vp_w = viewport_rect.width().max(1.0).min(width as f32);
|
||||
let vp_h = viewport_rect.height().max(1.0).min(height as f32);
|
||||
let vp_x = viewport_rect.min.x.max(0.0).min(width as f32 - vp_w);
|
||||
let vp_y = viewport_rect.min.y.max(0.0).min(height as f32 - vp_h);
|
||||
|
||||
// Depth must match the full color target's dimensions (the
|
||||
// attachment binding requires that). Pixels outside `set_viewport`
|
||||
// are never written, so the extra depth is wasted memory but never
|
||||
// incorrect.
|
||||
self.ensure_depth(device, width, height);
|
||||
self.ensure_object_capacity(device, objects.len() as u32);
|
||||
|
||||
// Globals — aspect comes from the viewport rect, not the target.
|
||||
let aspect = vp_w / vp_h;
|
||||
let view_proj = camera.view_projection(aspect, view_transform);
|
||||
let to_light = (-lighting.light.direction).normalize_or_zero();
|
||||
let lc = lighting.light.color;
|
||||
let amb = lighting.ambient;
|
||||
let globals = GlobalsUniform {
|
||||
view_proj: view_proj.to_cols_array_2d(),
|
||||
camera_pos: view_transform.translation.extend(1.0).to_array(),
|
||||
light_dir: to_light.extend(0.0).to_array(),
|
||||
light_color: (Vec4::new(lc.r, lc.g, lc.b, 1.0) * lighting.light.intensity).to_array(),
|
||||
ambient: Vec4::new(amb.r, amb.g, amb.b, 1.0).to_array(),
|
||||
};
|
||||
queue.write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
|
||||
|
||||
// Per-object uniforms.
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
let model = obj.transform.to_matrix();
|
||||
let normal_mtx = Mat3::from_mat4(model).inverse().transpose();
|
||||
let normal_mtx4 = [
|
||||
normal_mtx.x_axis.extend(0.0).to_array(),
|
||||
normal_mtx.y_axis.extend(0.0).to_array(),
|
||||
normal_mtx.z_axis.extend(0.0).to_array(),
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
let a = obj.material.albedo;
|
||||
let uniform = ObjectUniform {
|
||||
model: model.to_cols_array_2d(),
|
||||
normal_mtx: normal_mtx4,
|
||||
albedo: [a.r, a.g, a.b, a.a],
|
||||
mr: [obj.material.metallic, obj.material.roughness, 0.0, 0.0],
|
||||
};
|
||||
queue.write_buffer(
|
||||
&self.object_buffer,
|
||||
i as u64 * self.object_stride,
|
||||
bytemuck::bytes_of(&uniform),
|
||||
);
|
||||
}
|
||||
|
||||
let depth_view = &self.depth.as_ref().expect("depth ensured above").view;
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("oxide.forward.encoder"),
|
||||
});
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("oxide.forward.pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
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::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
// Restrict drawing to the host's viewport sub-rect. Pixels
|
||||
// outside this rectangle keep whatever the prior pass (e.g.
|
||||
// ClearPass or the window clear) wrote there.
|
||||
pass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.globals_bind_group, &[]);
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
let offset = (i as u64 * self.object_stride) as u32;
|
||||
pass.set_bind_group(1, &self.object_bind_group, &[offset]);
|
||||
pass.set_vertex_buffer(0, obj.mesh.vertex_buffer.slice(..));
|
||||
pass.set_index_buffer(obj.mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..obj.mesh.index_count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
queue.submit([encoder.finish()]);
|
||||
}
|
||||
|
||||
fn ensure_depth(&mut self, device: &wgpu::Device, width: u32, height: u32) {
|
||||
let stale = match &self.depth {
|
||||
Some(d) => d.width != width || d.height != height,
|
||||
None => true,
|
||||
};
|
||||
if stale {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("oxide.forward.depth"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
self.depth = Some(DepthTarget {
|
||||
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_object_capacity(&mut self, device: &wgpu::Device, needed: u32) {
|
||||
if needed > self.object_capacity {
|
||||
let capacity = needed.next_power_of_two();
|
||||
let (buffer, bind_group) =
|
||||
create_object_storage(device, &self.object_layout, self.object_stride, capacity);
|
||||
self.object_buffer = buffer;
|
||||
self.object_bind_group = bind_group;
|
||||
self.object_capacity = capacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates the per-object uniform buffer (`capacity` slots of `stride` bytes)
|
||||
/// and a dynamic-offset bind group over it.
|
||||
fn create_object_storage(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
stride: u64,
|
||||
capacity: u32,
|
||||
) -> (wgpu::Buffer, wgpu::BindGroup) {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("oxide.forward.objects"),
|
||||
size: stride * capacity as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("oxide.forward.object_bg"),
|
||||
layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &buffer,
|
||||
offset: 0,
|
||||
size: NonZeroU64::new(std::mem::size_of::<ObjectUniform>() as u64),
|
||||
}),
|
||||
}],
|
||||
});
|
||||
(buffer, bind_group)
|
||||
}
|
||||
|
||||
/// Rounds `value` up to the next multiple of `align` (a power of two).
|
||||
fn align_up(value: u64, align: u64) -> u64 {
|
||||
let align = align.max(1);
|
||||
value.div_ceil(align) * align
|
||||
}
|
||||
Reference in New Issue
Block a user