何时阅读:增删 Pass;改顶点格式;改着色器接口;调资源生命周期;优化渲染性能 关联文档:
README.md·architecture.md·features/meshing.md·features/ui.md·reference.md
render crate 封装 wgpu,对外提供:
- 与
<canvas>绑定的Renderer入口 - Render Graph 多 Pass 调度
- Chunk 网格生成(贪婪算法 + 跨区块面剔除 + u32 顶点压缩)
- 纹理图集 + 深度纹理 + Uniform Buffer 资源管理
不负责:
- 输入处理(→
client::input) - 世界数据持有(仅引用
core::Chunk/ 玩家位置;持有方是client/server) - 网络(→
net) - UI 业务(→
client::ui);本 crate 仅提供 egui 渲染 Pass 容器
crates/render/src/
├── lib.rs Renderer 入口 + 公开 API
├── device.rs Surface/Device 与 canvas 绑定
├── graph.rs Render Graph 调度框架(trait,当前未接入渲染路径)
├── passes/
│ ├── mod.rs
│ ├── opaque.rs 实体方块 Pass
│ ├── skybox.rs 天空盒 Pass(Phase 8 实装)
│ ├── transparent.rs 半透明 Pass(Phase 8 实装)
│ └── selection.rs 选中方块线框 Pass(Phase 3)
├── chunk_mesh.rs 贪婪网格化 + 跨区块面剔除 + AO + bounds(Phase 7)
├── vertex.rs u32 压缩格式 + 解包工具
├── texture.rs 纹理图集
└── shaders/
├── chunk.wgsl 实体方块着色器
└── selection.wgsl 选中线框着色器
职责:
- 创建
wgpu::Instance(Backends::BROWSER_WEBGPU) - 通过
web-sys::HtmlCanvasElement创建Surface - 协商
Adapter/Device/Queue - 监听 canvas resize 事件,重建 Surface 配置和 depth texture
- 提供首选纹理格式查询(一般是
Bgra8Unorm或Rgba8UnormSrgb)
关键 API:
pub struct DeviceContext {
pub surface: wgpu::Surface<'static>,
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub surface_format: wgpu::TextureFormat,
}
/// 异步初始化,需在 wasm-bindgen-futures 的 spawn_local 中调用
pub async fn init_device(canvas: &web_sys::HtmlCanvasElement) -> Result<DeviceContext, String>;WebGPU 特定注意:
Backends::BROWSER_WEBGPU不要写成Backends::all(),避免 wgpu 试图启用桌面后端request_adapter在 Firefox 稳定版会失败 → 在 client 层捕获并提示用户- HiDPI:监听
window.devicePixelRatio,乘上 canvas 逻辑尺寸传给resize - canvas 大小变化通过
ResizeObserver监听(client 层负责)
详细布局见 features/meshing.md 顶点压缩章节。本文只列接口:
/// 压缩后的顶点:单 u32
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct PackedVertex(pub u32);
impl PackedVertex {
pub fn pack(local_x: u8, local_y: u8, local_z: u8,
face: Face, texture: u8, ao: u8) -> Self;
}
#[repr(u8)]
pub enum Face { PosX = 0, NegX = 1, PosY = 2, NegY = 3, PosZ = 4, NegZ = 5 }
/// wgpu 顶点缓冲布局描述(u32 attribute)
pub fn vertex_buffer_layout() -> wgpu::VertexBufferLayout<'static>;WGSL 解包:
struct UnpackedVertex {
local_pos: vec3<f32>,
face_normal: vec3<f32>,
texture_uv: vec2<f32>,
ao_factor: f32,
}
fn unpack_vertex(packed: u32, chunk_origin: vec3<f32>) -> UnpackedVertex { ... }设计:
- 单张大图(如 256×256,每格 16×16 → 16×16 = 256 个纹理槽,足够本期所有方块)
- 每方块的纹理由
BlockProperties.texture_index: u8指定槽位 - WGSL 中 UV 计算:
uv = (texture_index_to_grid(idx) + face_local_uv) / atlas_size
纹理来源:
- 本期:项目内嵌(
include_bytes!加载,编译进 wasm) - v2:可选远程加载(fetch + Image bitmap)
Mipmap:本期不开(避免远处纹理颜色混叠产生彩虹),用 MagFilter::Nearest + MinFilter::Nearest 保持像素风。
- 声明式:每个 Pass 声明输入资源(贴图/UB)+ 输出资源(render target)
- 依赖排序:自动按依赖关系排序 Pass
- 资源复用:同一 frame 内的中间贴图(如 SSAO 输出)可共享
- 简化版:本期不实现完整 DAG 调度,使用固定顺序(Depth → Opaque → Skybox → Transparent → UI);预留 trait 便于后期扩展
pub trait RenderPass {
fn name(&self) -> &'static str;
/// 每帧调用,编码 GPU 命令
fn execute(&mut self, ctx: &mut PassContext);
}
pub struct PassContext<'a> {
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
pub encoder: &'a mut wgpu::CommandEncoder,
pub surface_view: &'a wgpu::TextureView,
pub depth_view: &'a wgpu::TextureView,
pub camera_bind_group: &'a wgpu::BindGroup,
pub frame_data: &'a FrameData, // 玩家位置、时间、待渲染 chunk 列表等
}pub struct RenderGraph {
passes: Vec<Box<dyn RenderPass>>,
}
impl RenderGraph {
pub fn new() -> Self;
pub fn add_pass(&mut self, pass: Box<dyn RenderPass>);
pub fn execute(&mut self, ctx: &mut PassContext);
}调度模式:单 CommandEncoder,所有 Pass 共享,最后一次 queue.submit()。
let mut graph = RenderGraph::new();
graph.add_pass(Box::new(DepthPrePass::new(...))); // 可通过 settings 关闭
graph.add_pass(Box::new(OpaquePass::new(...)));
graph.add_pass(Box::new(SkyboxPass::new(...)));
graph.add_pass(Box::new(TransparentPass::new(...)));
graph.add_pass(Box::new(UiPass::new(...))); // egui-wgpu 容器目的:先渲染所有不透明几何体的深度,让后续 Opaque Pass 享受 Early-Z 优化(减少 overdraw)。
输入:所有可见 Chunk 网格 + 相机 UB
输出:写入 depth texture(LessEqual 测试);color attachment 留空(store_op = Discard)
着色器:极简顶点着色器(只算 clip_position,无片段着色)
何时关闭:低端 GPU 或场景简单时,Depth Pre-Pass 可能反而变慢;提供运行时开关 RenderSettings::depth_prepass_enabled。
目的:渲染所有不透明方块。
输入:可见 Chunk 网格 + 纹理图集 + 相机 UB
输出:写入 color + depth(Less,store)
Pipeline 设置:
- 深度比较:
Less(如果有 Depth Pre-Pass,可改Equal进一步省 fragment work) - Cull 模式:当前为
None(贪婪网格 winding 已保持外侧 CCW,但 Phase 7 优先保证可视正确性;后续可单独启用 Back-face culling 验证收益) - Blend:禁用
- 顶点格式:
PackedVertex(u32)+u32index buffer
Draw 调用顺序:Phase 7 先做视锥剔除,并在单个 render pass 内遍历可见 chunk 调用 draw_indexed;近远排序留作后续 profiling 项。
目的:填充背景天空(程序化天空,支持太阳方向 + 颜色梯度)。 绘制方式:
- 全屏三角形(覆盖整个 viewport)
- 片段着色器根据 ray direction 计算颜色(线性 azimuth-elevation 渐变)
- 深度比较:
LessEqual,深度写入:false(保证天空不挡其它东西,但被前景挡住)
程序化天空算法:
fn sky_color(dir: vec3<f32>, sun_dir: vec3<f32>) -> vec3<f32> {
let sun_dot = max(dot(dir, sun_dir), 0.0);
let zenith = mix(horizon_color, zenith_color, smoothstep(0.0, 0.6, dir.y));
let sun_glow = pow(sun_dot, 64.0) * sun_color;
return zenith + sun_glow;
}v2 stretch:可替换为 cubemap(加载 6 张 png)。
目的:渲染水、玻璃等半透明方块。 关键差异:
- Blend:
Alpha Blending(SrcAlpha, OneMinusSrcAlpha) - 深度比较:
Less,深度写入:false - Draw 顺序:按距离从远到近排序(保证混合顺序)
- 网格独立:透明方块不参与贪婪网格化的合并(不同方块属性不能合并);用单独的 mesh buffer
简化策略:本期透明方块不超过 2 种(水 + 玻璃),不实现"Order-Independent Transparency"等高级技术。
目的:把 egui 渲染输出叠到画面上。
实现:直接复用 egui-wgpu 的 Renderer,把它包装为 RenderPass。
关键参数:
LoadOp::Load(不清屏,叠加到已有画面)- 深度测试:禁用(UI 不参与 3D 深度)
输入:每帧由 client 层提供的 egui::FullOutput(PaintJobs)
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct CameraUniform {
pub view_proj: [[f32; 4]; 4],
pub view: [[f32; 4]; 4], // 单独提供给天空盒(去平移)
pub camera_pos: [f32; 4], // xyz + 1.0 padding
pub time_seconds: f32, // 用于水波动画等
pub _padding: [f32; 3],
}每帧由 client 层调用 Renderer::update_camera(&CameraUniform),写入 GPU buffer,绑定到所有 Pass 的 group(0)。
pub struct Renderer {
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub surface: wgpu::Surface<'static>,
pub surface_format: wgpu::TextureFormat,
// 内部字段:width, height, depth_texture, depth_view,
// opaque_pass: OpaquePass, selection_pass: SelectionPass,
// chunk_meshes: HashMap<ChunkPos, ChunkMeshGpu>
}
impl Renderer {
pub async fn new(canvas: &web_sys::HtmlCanvasElement) -> Result<Self, String>;
pub fn resize(&mut self, width: u32, height: u32);
/// 上传或更新一个 chunk 的网格(由 mesh job 完成后调用)
pub fn upload_chunk_mesh(&mut self, pos: ChunkPos, mesh: &ChunkMeshCpu);
/// 卸载远处 chunk 网格
pub fn drop_chunk_mesh(&mut self, pos: ChunkPos);
/// 退出当前世界 / 进入新世界前清空所有世界渲染缓存
pub fn clear_world_cache(&mut self);
/// 查询某个 chunk 是否已有 GPU mesh
pub fn has_chunk_mesh(&self, pos: ChunkPos) -> bool;
/// 取得本帧 surface texture(失败时自动重配 Surface)
pub fn acquire_frame(&mut self) -> Option<wgpu::SurfaceTexture>;
/// 渲染世界(OpaquePass):清屏 + 视锥剔除 + 单 render pass 内 draw_indexed
pub fn render_world(&mut self, encoder: &mut wgpu::CommandEncoder,
color_view: &wgpu::TextureView, view_proj: Mat4, clear_color: [f64; 4])
-> WorldRenderStats;
/// 渲染选中方块线框(在 render_world 之后调用)
pub fn render_selection(&mut self, encoder: &mut wgpu::CommandEncoder,
color_view: &wgpu::TextureView, view_proj: Mat4, block_pos: Option<Position>);
pub fn depth_view(&self) -> &wgpu::TextureView;
pub fn loaded_chunk_count(&self) -> usize;
pub fn uploaded_vertex_count(&self) -> u32;
pub fn uploaded_index_count(&self) -> u32;
}Phase 8 更新:Renderer 已接入固定顺序多 Pass:Skybox → Depth Pre-Pass(可关)→ Opaque → Player → Transparent → Selection → UI(UI 仍由 client 持有 egui-wgpu renderer 编码)。透明方块拥有独立 mesh buffer,并按 chunk 中心到相机距离远到近绘制。
RenderGraphtrait 继续作为扩展点保留,当前主路径使用显式方法调用以减少 egui 生命周期改动。Pass 耗时仍是 CPU 编码耗时,不是 GPU timestamp query。
| 资源 | 创建时机 | 销毁时机 |
|---|---|---|
Surface / Device / Queue |
Renderer::new 一次 |
Tab 关闭 |
depth_texture |
启动 + 每次 resize | 重建时 |
chunk_mesh_gpu |
upload_chunk_mesh |
drop_chunk_mesh(玩家走远)/ clear_world_cache(退出或切换世界)/ chunk 修改时(重建) |
| Pass pipelines | 各 Pass new 一次 |
程序退出 |
Chunk 网格更新规则:
- 方块修改 →
client::game把对应 chunk 加入 dirty 集合 - 下一帧 mesh budget:取出 dirty chunk →
chunk_mesh::generate_with_neighbors生成 CPU 数据 →Renderer::upload_chunk_mesh上传 GPU - 玩家走远(超出渲染距离)→
Renderer::drop_chunk_mesh卸载
| 项目 | 目标 |
|---|---|
| 单帧总时间 | < 16.6ms(60fps) |
| Render Graph 执行(CPU 编码) | < 2ms |
| GPU draw(典型场景,渲染距离 6) | < 8ms |
| 网格化(每帧最多) | 4ms |
| 其余(输入、相机、UI build) | < 2ms |
监测手段:AppSettings.show_stats 开启时,HUD 显示 mesh / world / player / selection / ui 的 CPU 编码耗时,以及视锥剔除和 draw 顶点/索引统计。详见 features/ui.md。
- 阴影贴图(v2 / stretch)
- SSAO(v2 / stretch)
- Bloom / 色调映射(v2 / stretch)
- 粒子系统
- 镜面反射 / 屏幕空间反射
- 体积光
- WebGL2 后端
- 着色器热重载(见
README.mdOut-of-Scope)