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:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit f56a1eea3b
128 changed files with 40493 additions and 2 deletions
+435
View File
@@ -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
}