Phase 3: extract render/ module from state.rs
state.rs is now 870 lines (down from 1700+). All GPU code moved to a
dedicated render/ module.
New src/render/:
mod.rs Renderer struct + impl with all methods; the only
place that owns wgpu device/surface/pipelines. The
wasm-only WebGPU probe + compat-error overlay live
here as a private wasm_compat sub-module.
pipelines.rs Pipeline factory functions: camera_bgl, post_bgl,
pipeline_layout, sky_pipeline, terrain_pipeline,
outline_pipeline, post_pipeline. Each takes
device + shader + format and returns a configured
wgpu::RenderPipeline — pure factories, no hidden
state.
scene_target.rs create_depth_view, create_scene_color_view,
create_post_bind_group — fresh-resource builders
called from Renderer::new and Renderer::resize.
uniform.rs CameraUniform (matches WGSL camera.frame layout),
OutlineVertex, ChunkBuffers.
state.rs:
- drops ~760 lines of pipeline + scene-target plumbing
- imports Renderer from crate::render
- keeps App + tick + drain_net_inbox + thread_locals + wasm_api
Tests: 53/53 pass. Native + wasm release build clean.
This commit is contained in:
parent
accbf67bf2
commit
549662ddc8
6 changed files with 988 additions and 885 deletions
|
|
@ -2,6 +2,7 @@ pub mod camera;
|
|||
pub mod mesh;
|
||||
pub mod net;
|
||||
pub mod proto;
|
||||
pub mod render;
|
||||
pub mod shader_source;
|
||||
pub mod sim;
|
||||
pub mod state;
|
||||
|
|
|
|||
631
src/render/mod.rs
Normal file
631
src/render/mod.rs
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
//! GPU shell. Owns the wgpu device + surface + pipelines + per-chunk
|
||||
//! buffers + remote-player buffers + offscreen scene-color target +
|
||||
//! post pipeline. Everything visual happens here; pure simulation
|
||||
//! (sim/) and net parsing (net/) feed it via the App tick.
|
||||
//!
|
||||
//! Sub-modules:
|
||||
//! pipelines.rs pipeline + bind-group-layout factories
|
||||
//! scene_target.rs depth / scene-color / post bind-group helpers
|
||||
//! uniform.rs CameraUniform + OutlineVertex + ChunkBuffers
|
||||
pub mod pipelines;
|
||||
pub mod scene_target;
|
||||
pub mod uniform;
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::mesh::{build_chunk_mesh, emit_oriented_box, name_hash, Vertex};
|
||||
use crate::state::RemotePlayer;
|
||||
use crate::world::World;
|
||||
use glam::{IVec3, Vec3};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use uniform::{CameraUniform, ChunkBuffers, OutlineVertex};
|
||||
use wgpu::util::DeviceExt;
|
||||
use winit::window::Window;
|
||||
|
||||
const OUTLINE_VERT_COUNT: u64 = 24;
|
||||
const MAX_REMOTE_PLAYERS: u64 = 32;
|
||||
// 2 boxes per remote player (body + head); each box = 6 faces × 4 verts
|
||||
// and 6 faces × 6 indices.
|
||||
const REMOTE_VERTS_PER_PLAYER: u64 = 2 * 24;
|
||||
const REMOTE_INDICES_PER_PLAYER: u64 = 2 * 36;
|
||||
|
||||
pub struct Renderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
sky_pipeline: wgpu::RenderPipeline,
|
||||
outline_pipeline: wgpu::RenderPipeline,
|
||||
post_pipeline: wgpu::RenderPipeline,
|
||||
|
||||
outline_buffer: wgpu::Buffer,
|
||||
depth_view: wgpu::TextureView,
|
||||
camera_buffer: wgpu::Buffer,
|
||||
camera_bind_group: wgpu::BindGroup,
|
||||
pub window: Arc<Window>,
|
||||
chunk_buffers: HashMap<IVec3, ChunkBuffers>,
|
||||
outline_target: Option<IVec3>,
|
||||
visible_chunks: Vec<IVec3>,
|
||||
remote_vb: wgpu::Buffer,
|
||||
remote_ib: wgpu::Buffer,
|
||||
remote_index_count: u32,
|
||||
|
||||
// ---- Post processing (Step 1: pass-through scene → surface) ----
|
||||
scene_color: wgpu::TextureView,
|
||||
scene_color_format: wgpu::TextureFormat,
|
||||
post_sampler: wgpu::Sampler,
|
||||
post_bgl: wgpu::BindGroupLayout,
|
||||
post_bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub async fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
#[allow(unused_mut)]
|
||||
let (mut width, mut height) = (size.width.max(1), size.height.max(1));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if width <= 2 || height <= 2 {
|
||||
if let Some(c) = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| d.get_element_by_id("game-canvas"))
|
||||
.and_then(|e| {
|
||||
wasm_bindgen::JsCast::dyn_into::<web_sys::HtmlCanvasElement>(e).ok()
|
||||
})
|
||||
{
|
||||
let w = c.width().max(1);
|
||||
let h = c.height().max(1);
|
||||
log::info!("overriding inner_size {}x{} with canvas {}x{}", width, height, w, h);
|
||||
width = w;
|
||||
height = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("initial surface size: {}x{}", width, height);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let backends = {
|
||||
let webgpu_ok = wasm_compat::detect_webgpu().await;
|
||||
if webgpu_ok {
|
||||
log::info!("WebGPU adapter probe OK — using WebGPU backend");
|
||||
wgpu::Backends::BROWSER_WEBGPU
|
||||
} else {
|
||||
log::info!("WebGPU unavailable — using WebGL2 backend");
|
||||
wgpu::Backends::GL
|
||||
}
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let backends = wgpu::Backends::PRIMARY | wgpu::Backends::GL;
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = match instance.create_surface(window.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("create_surface failed: {e:?}");
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_compat::show_browser_compat_error(&format!("{e}"));
|
||||
panic!("could not create rendering surface: {e:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let adapter = match instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Some(a) => a,
|
||||
None => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_compat::show_browser_compat_error(
|
||||
"No GPU adapter available. WebGL2/WebGPU may be disabled in your browser.",
|
||||
);
|
||||
panic!("no suitable GPU adapter");
|
||||
}
|
||||
};
|
||||
|
||||
let info = adapter.get_info();
|
||||
log::info!(
|
||||
"wgpu adapter: backend={:?} type={:?} name={:?}",
|
||||
info.backend,
|
||||
info.device_type,
|
||||
info.name,
|
||||
);
|
||||
let primary_limits = if matches!(info.backend, wgpu::Backend::BrowserWebGpu) {
|
||||
wgpu::Limits::default()
|
||||
} else {
|
||||
wgpu::Limits::downlevel_webgl2_defaults()
|
||||
};
|
||||
|
||||
let (device, queue) = match adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: Some("device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: primary_limits,
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(dq) => dq,
|
||||
Err(e) => {
|
||||
log::warn!("device request failed ({e:?}); retrying with downlevel limits");
|
||||
adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: Some("device-fallback"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("device request failed even with downlevel limits")
|
||||
}
|
||||
};
|
||||
|
||||
let caps = surface.get_capabilities(&adapter);
|
||||
let format = caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(caps.formats[0]);
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let depth_view = scene_target::create_depth_view(&device, width, height);
|
||||
|
||||
// ---- Shaders (terrain + post) ----
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("terrain shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
crate::shader_source::terrain_shader_source().into(),
|
||||
),
|
||||
});
|
||||
let post_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("post shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(crate::shader_source::post_shader_source().into()),
|
||||
});
|
||||
|
||||
// ---- Camera uniform + layouts + pipelines ----
|
||||
let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("camera"),
|
||||
contents: bytemuck::bytes_of(&CameraUniform::identity()),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let camera_bgl = pipelines::camera_bgl(&device);
|
||||
let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("camera bg"),
|
||||
layout: &camera_bgl,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: camera_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let pl = pipelines::pipeline_layout(&device, "pl", &[&camera_bgl]);
|
||||
|
||||
let sky_pipeline = pipelines::sky_pipeline(&device, &pl, &shader, config.format);
|
||||
let pipeline = pipelines::terrain_pipeline(&device, &pl, &shader, config.format);
|
||||
let outline_pipeline = pipelines::outline_pipeline(&device, &pl, &shader, config.format);
|
||||
|
||||
// ---- Buffers for outline + remote players ----
|
||||
let outline_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("outline buf"),
|
||||
size: OUTLINE_VERT_COUNT * std::mem::size_of::<OutlineVertex>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let remote_vb = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("remote vb"),
|
||||
size: MAX_REMOTE_PLAYERS * REMOTE_VERTS_PER_PLAYER
|
||||
* std::mem::size_of::<Vertex>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let remote_ib = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("remote ib"),
|
||||
size: MAX_REMOTE_PLAYERS * REMOTE_INDICES_PER_PLAYER * 4,
|
||||
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// ---- Post pipeline + scene-color target ----
|
||||
let scene_color_format = config.format;
|
||||
let scene_color =
|
||||
scene_target::create_scene_color_view(&device, width, height, scene_color_format);
|
||||
let post_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("post sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
let post_bgl = pipelines::post_bgl(&device);
|
||||
let post_bind_group =
|
||||
scene_target::create_post_bind_group(&device, &post_bgl, &scene_color, &post_sampler);
|
||||
let post_pl = pipelines::pipeline_layout(&device, "post pl", &[&post_bgl]);
|
||||
let post_pipeline = pipelines::post_pipeline(&device, &post_pl, &post_shader, config.format);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
pipeline,
|
||||
sky_pipeline,
|
||||
outline_pipeline,
|
||||
outline_buffer,
|
||||
depth_view,
|
||||
camera_buffer,
|
||||
camera_bind_group,
|
||||
window,
|
||||
chunk_buffers: HashMap::new(),
|
||||
outline_target: None,
|
||||
visible_chunks: Vec::new(),
|
||||
remote_vb,
|
||||
remote_ib,
|
||||
remote_index_count: 0,
|
||||
scene_color,
|
||||
scene_color_format,
|
||||
post_sampler,
|
||||
post_bgl,
|
||||
post_bind_group,
|
||||
post_pipeline,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_remote_players(&mut self, players: &[RemotePlayer]) {
|
||||
if players.is_empty() {
|
||||
self.remote_index_count = 0;
|
||||
return;
|
||||
}
|
||||
let mut verts: Vec<Vertex> = Vec::with_capacity(players.len() * 48);
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(players.len() * 72);
|
||||
let max = players.len().min(MAX_REMOTE_PLAYERS as usize);
|
||||
for p in &players[..max] {
|
||||
let h = name_hash(&p.name);
|
||||
let r = 0.35 + (h & 0x3F) as f32 / 255.0;
|
||||
let g = 0.35 + ((h >> 8) & 0x3F) as f32 / 255.0;
|
||||
let b = 0.35 + ((h >> 16) & 0x3F) as f32 / 255.0;
|
||||
let body = [r, g, b];
|
||||
let head = [r * 0.85 + 0.15, g * 0.85 + 0.15, b * 0.85 + 0.15];
|
||||
emit_oriented_box(
|
||||
Vec3::new(p.pos.x, p.pos.y + 0.65, p.pos.z),
|
||||
Vec3::new(0.3, 0.65, 0.2),
|
||||
p.yaw,
|
||||
body,
|
||||
&mut verts,
|
||||
&mut indices,
|
||||
);
|
||||
emit_oriented_box(
|
||||
Vec3::new(p.pos.x, p.pos.y + 1.55, p.pos.z),
|
||||
Vec3::new(0.25, 0.25, 0.25),
|
||||
p.yaw,
|
||||
head,
|
||||
&mut verts,
|
||||
&mut indices,
|
||||
);
|
||||
}
|
||||
self.remote_index_count = indices.len() as u32;
|
||||
self.queue
|
||||
.write_buffer(&self.remote_vb, 0, bytemuck::cast_slice(&verts));
|
||||
self.queue
|
||||
.write_buffer(&self.remote_ib, 0, bytemuck::cast_slice(&indices));
|
||||
}
|
||||
|
||||
pub fn set_outline(&mut self, target: Option<IVec3>) {
|
||||
if self.outline_target == target {
|
||||
return;
|
||||
}
|
||||
self.outline_target = target;
|
||||
if let Some(b) = target {
|
||||
let eps = 0.002;
|
||||
let min = [b.x as f32 - eps, b.y as f32 - eps, b.z as f32 - eps];
|
||||
let max = [
|
||||
b.x as f32 + 1.0 + eps,
|
||||
b.y as f32 + 1.0 + eps,
|
||||
b.z as f32 + 1.0 + eps,
|
||||
];
|
||||
let c000 = [min[0], min[1], min[2]];
|
||||
let c100 = [max[0], min[1], min[2]];
|
||||
let c001 = [min[0], min[1], max[2]];
|
||||
let c101 = [max[0], min[1], max[2]];
|
||||
let c010 = [min[0], max[1], min[2]];
|
||||
let c110 = [max[0], max[1], min[2]];
|
||||
let c011 = [min[0], max[1], max[2]];
|
||||
let c111 = [max[0], max[1], max[2]];
|
||||
let verts = [
|
||||
c000, c100, c100, c101, c101, c001, c001, c000, c010, c110, c110, c111, c111,
|
||||
c011, c011, c010, c000, c010, c100, c110, c101, c111, c001, c011,
|
||||
];
|
||||
let data: Vec<OutlineVertex> = verts.iter().map(|p| OutlineVertex { pos: *p }).collect();
|
||||
self.queue
|
||||
.write_buffer(&self.outline_buffer, 0, bytemuck::cast_slice(&data));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
if width == 0 || height == 0 {
|
||||
return;
|
||||
}
|
||||
self.config.width = width;
|
||||
self.config.height = height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.depth_view = scene_target::create_depth_view(&self.device, width, height);
|
||||
self.scene_color = scene_target::create_scene_color_view(
|
||||
&self.device,
|
||||
width,
|
||||
height,
|
||||
self.scene_color_format,
|
||||
);
|
||||
self.post_bind_group = scene_target::create_post_bind_group(
|
||||
&self.device,
|
||||
&self.post_bgl,
|
||||
&self.scene_color,
|
||||
&self.post_sampler,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn rebuild_chunk(&mut self, coord: IVec3, world: &World) {
|
||||
let Some(chunk) = world.chunks.get(&coord) else {
|
||||
return;
|
||||
};
|
||||
let mesh = build_chunk_mesh(world, chunk);
|
||||
if mesh.indices.is_empty() {
|
||||
self.chunk_buffers.remove(&coord);
|
||||
return;
|
||||
}
|
||||
let vertex = self
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("chunk vb"),
|
||||
contents: bytemuck::cast_slice(&mesh.vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index = self
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("chunk ib"),
|
||||
contents: bytemuck::cast_slice(&mesh.indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
self.chunk_buffers.insert(
|
||||
coord,
|
||||
ChunkBuffers {
|
||||
vertex,
|
||||
index,
|
||||
index_count: mesh.indices.len() as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn upload_camera(&self, camera: &Camera, time: f32) {
|
||||
let vp = camera.view_proj();
|
||||
let inv = vp.inverse();
|
||||
let uni = CameraUniform {
|
||||
view_proj: vp.to_cols_array_2d(),
|
||||
inv_view_proj: inv.to_cols_array_2d(),
|
||||
eye: [camera.position.x, camera.position.y, camera.position.z, 1.0],
|
||||
frame: [time, 0.0, 0.0, 0.0],
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(&uni));
|
||||
}
|
||||
|
||||
pub fn set_visible(&mut self, chunks: Vec<IVec3>) {
|
||||
self.visible_chunks = chunks;
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Result<(), wgpu::SurfaceError> {
|
||||
let frame = self.surface.get_current_texture()?;
|
||||
let surface_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("enc") });
|
||||
|
||||
// ---- Scene pass: render the world into the offscreen scene_color. ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("scene pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.scene_color,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.30,
|
||||
g: 0.55,
|
||||
b: 0.88,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.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,
|
||||
});
|
||||
pass.set_pipeline(&self.sky_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
let iter: Box<dyn Iterator<Item = &ChunkBuffers>> = if self.visible_chunks.is_empty() {
|
||||
Box::new(self.chunk_buffers.values())
|
||||
} else {
|
||||
Box::new(self.visible_chunks.iter().filter_map(|c| self.chunk_buffers.get(c)))
|
||||
};
|
||||
for buf in iter {
|
||||
pass.set_vertex_buffer(0, buf.vertex.slice(..));
|
||||
pass.set_index_buffer(buf.index.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..buf.index_count, 0, 0..1);
|
||||
}
|
||||
if self.remote_index_count > 0 {
|
||||
pass.set_vertex_buffer(0, self.remote_vb.slice(..));
|
||||
pass.set_index_buffer(self.remote_ib.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..self.remote_index_count, 0, 0..1);
|
||||
}
|
||||
if self.outline_target.is_some() {
|
||||
pass.set_pipeline(&self.outline_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.outline_buffer.slice(..));
|
||||
pass.draw(0..OUTLINE_VERT_COUNT as u32, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Post pass: scene_color → surface (effects later). ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("post pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &surface_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.post_pipeline);
|
||||
pass.set_bind_group(0, &self.post_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Browser-only compat helpers (probe WebGPU, render init-failure overlay) ----
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm_compat {
|
||||
pub async fn detect_webgpu() -> bool {
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
let Some(win) = web_sys::window() else {
|
||||
log::info!("detect_webgpu: no window");
|
||||
return false;
|
||||
};
|
||||
let nav: JsValue = win.navigator().into();
|
||||
let gpu = match js_sys::Reflect::get(&nav, &JsValue::from_str("gpu")) {
|
||||
Ok(v) if !v.is_undefined() && !v.is_null() => v,
|
||||
_ => {
|
||||
log::info!("detect_webgpu: navigator.gpu missing");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let req = match js_sys::Reflect::get(&gpu, &JsValue::from_str("requestAdapter")) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter prop missing");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let req_fn: js_sys::Function = match req.dyn_into() {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter not callable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let promise = match req_fn.call0(&gpu) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::info!("detect_webgpu: requestAdapter threw: {e:?}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let promise: js_sys::Promise = match promise.dyn_into() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter did not return Promise");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||
Ok(adapter) => {
|
||||
let ok = !adapter.is_null() && !adapter.is_undefined();
|
||||
log::info!("detect_webgpu: requestAdapter resolved (adapter present = {ok})");
|
||||
ok
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("detect_webgpu: requestAdapter rejected: {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_browser_compat_error(detail: &str) {
|
||||
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
||||
if let Some(body) = doc.body() {
|
||||
let html = format!(
|
||||
"<div id=\"compat-error\" style=\"\
|
||||
position:fixed; inset:0; background:rgba(0,0,0,0.85);\
|
||||
color:#f0f0f0; padding:40px; font-family:system-ui,sans-serif;\
|
||||
z-index:1000; overflow:auto;\">\
|
||||
<h1 style=\"margin-top:0;color:#f88;\">Couldn't start the renderer</h1>\
|
||||
<p>Your browser couldn't open a WebGL2 or WebGPU context on the canvas.</p>\
|
||||
<p><strong>Chrome / Chromium / Edge:</strong> open \
|
||||
<code>chrome://gpu</code> and confirm \"WebGL 2\" says \
|
||||
<em>Hardware accelerated</em>. If not, enable hardware acceleration in \
|
||||
<code>chrome://settings/system</code>, or set \
|
||||
<code>chrome://flags/#ignore-gpu-blocklist</code> to <em>Enabled</em>.\
|
||||
</p>\
|
||||
<p><strong>LibreWolf / Firefox:</strong> in <code>about:config</code> set \
|
||||
<code>webgl.disabled</code> = false, \
|
||||
<code>webgl.force-enabled</code> = true, \
|
||||
<code>privacy.resistFingerprinting</code> = false.\
|
||||
</p>\
|
||||
<p style=\"opacity:0.65;font-size:12px;\">Underlying error: {detail}</p>\
|
||||
</div>",
|
||||
detail = html_escape(detail)
|
||||
);
|
||||
let _ = body.insert_adjacent_html("beforeend", &html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn html_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
}
|
||||
237
src/render/pipelines.rs
Normal file
237
src/render/pipelines.rs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
//! Pipeline + bind-group-layout factories. Each `*_pipeline` function
|
||||
//! takes device + shader + surface format and returns a configured
|
||||
//! `wgpu::RenderPipeline`. The renderer wires them up in `Renderer::new`.
|
||||
use crate::mesh::Vertex;
|
||||
use crate::render::uniform::OutlineVertex;
|
||||
use wgpu::{
|
||||
BindGroupLayout, ColorTargetState, Device, FragmentState, PipelineLayout, RenderPipeline,
|
||||
ShaderModule, TextureFormat, VertexState,
|
||||
};
|
||||
|
||||
pub fn camera_bgl(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("camera bgl"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn post_bgl(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("post bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pipeline_layout(
|
||||
device: &Device,
|
||||
label: &str,
|
||||
layouts: &[&BindGroupLayout],
|
||||
) -> PipelineLayout {
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some(label),
|
||||
bind_group_layouts: layouts,
|
||||
push_constant_ranges: &[],
|
||||
})
|
||||
}
|
||||
|
||||
fn color_target(format: TextureFormat) -> ColorTargetState {
|
||||
ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-screen sky background. No vertex buffer; the shader emits a
|
||||
/// covering triangle from `vertex_index`. Depth always passes (drawn
|
||||
/// before terrain so terrain naturally overwrites it where present).
|
||||
pub fn sky_pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
shader: &ShaderModule,
|
||||
format: TextureFormat,
|
||||
) -> RenderPipeline {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sky pipeline"),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_sky"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_sky"),
|
||||
targets: &[Some(color_target(format))],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Terrain + remote-player meshes. Standard depth-tested back-face-
|
||||
/// culled triangles consuming `mesh::Vertex` layout.
|
||||
pub fn terrain_pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
shader: &ShaderModule,
|
||||
format: TextureFormat,
|
||||
) -> RenderPipeline {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("terrain pipeline"),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Vertex::LAYOUT],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(color_target(format))],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Targeted block outline — 12 line segments around an AABB. Uses
|
||||
/// `LessEqual` depth so it draws on top of the face it borders.
|
||||
pub fn outline_pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
shader: &ShaderModule,
|
||||
format: TextureFormat,
|
||||
) -> RenderPipeline {
|
||||
let outline_layout = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<OutlineVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
|
||||
};
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("outline pipeline"),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_outline"),
|
||||
buffers: &[outline_layout],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_outline"),
|
||||
targets: &[Some(color_target(format))],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::LineList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Pass-through post: full-screen triangle that samples `scene_color`
|
||||
/// back to the surface. Foundation for FXAA / sun shafts / tonemap.
|
||||
pub fn post_pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
shader: &ShaderModule,
|
||||
format: TextureFormat,
|
||||
) -> RenderPipeline {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("post pipeline"),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_post"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_post"),
|
||||
targets: &[Some(color_target(format))],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
72
src/render/scene_target.rs
Normal file
72
src/render/scene_target.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
//! Off-screen render targets: depth buffer, scene-color HDR texture,
|
||||
//! and the post-pass bind group that samples scene-color back into the
|
||||
//! surface. All factories take device + dimensions and return a fresh
|
||||
//! resource — no hidden mutation. Renderer::resize calls these to
|
||||
//! recreate after a surface change.
|
||||
use wgpu::{Device, Sampler, TextureFormat, TextureView};
|
||||
|
||||
pub fn create_depth_view(device: &Device, w: u32, h: u32) -> TextureView {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("depth"),
|
||||
size: wgpu::Extent3d {
|
||||
width: w.max(1),
|
||||
height: h.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
/// Off-screen color target the world is rendered into. Same format as
|
||||
/// the surface so the post pass can sample it and write the result back
|
||||
/// without any conversion cost.
|
||||
pub fn create_scene_color_view(
|
||||
device: &Device,
|
||||
w: u32,
|
||||
h: u32,
|
||||
format: TextureFormat,
|
||||
) -> TextureView {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("scene color"),
|
||||
size: wgpu::Extent3d {
|
||||
width: w.max(1),
|
||||
height: h.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn create_post_bind_group(
|
||||
device: &Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
scene: &TextureView,
|
||||
sampler: &Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("post bg"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(scene),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
42
src/render/uniform.rs
Normal file
42
src/render/uniform.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//! GPU-side uniform + vertex layouts owned by the renderer.
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::Mat4;
|
||||
|
||||
/// Uniform pushed to `@group(0) @binding(0)` of the world shader.
|
||||
/// Layout mirrors WGSL `struct Camera` in `shader.wgsl`.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
pub struct CameraUniform {
|
||||
pub view_proj: [[f32; 4]; 4],
|
||||
pub inv_view_proj: [[f32; 4]; 4],
|
||||
pub eye: [f32; 4],
|
||||
/// `.x` = scene time in seconds (drives day/night cycle + leaf
|
||||
/// sway). `.y/.z/.w` reserved. Matches WGSL `camera.frame` with
|
||||
/// `scene_time()` accessor.
|
||||
pub frame: [f32; 4],
|
||||
}
|
||||
|
||||
impl CameraUniform {
|
||||
pub fn identity() -> Self {
|
||||
Self {
|
||||
view_proj: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
inv_view_proj: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
eye: [0.0; 4],
|
||||
frame: [0.0; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
pub struct OutlineVertex {
|
||||
pub pos: [f32; 3],
|
||||
}
|
||||
|
||||
/// Vertex buffer + index buffer + count for a single chunk's mesh.
|
||||
/// Owned by the Renderer's `chunk_buffers` map.
|
||||
pub struct ChunkBuffers {
|
||||
pub vertex: wgpu::Buffer,
|
||||
pub index: wgpu::Buffer,
|
||||
pub index_count: u32,
|
||||
}
|
||||
890
src/state.rs
890
src/state.rs
|
|
@ -1,11 +1,10 @@
|
|||
//! Imperative shell: owns the GPU resources, the winit window, and the
|
||||
//! JS bridges. Every tick threads the pure simulation core
|
||||
//! (`crate::sim`) and pure net parser (`crate::net`) through the
|
||||
//! world/renderer/network — this file should remain *thin*.
|
||||
//! Imperative shell: owns the App + JS bridges + winit handler.
|
||||
//! The Renderer lives in `crate::render`; pure logic in `crate::sim`
|
||||
//! and `crate::net`. This file's job is to compose them per tick.
|
||||
use crate::camera::{Camera, InputState, KbHeld};
|
||||
use crate::mesh::{build_chunk_mesh, emit_oriented_box, name_hash, Vertex};
|
||||
use crate::net::{parse_inbox, NetEvent};
|
||||
use crate::proto::{ClientMsg, EditRec};
|
||||
use crate::render::Renderer;
|
||||
use crate::sim::collision::{aabb_overlap_player, AabbI, EYE_HEIGHT};
|
||||
use crate::sim::edit::{apply_edit, block_from_u8, chunks_for_edit};
|
||||
use crate::sim::input::TouchBridge;
|
||||
|
|
@ -14,14 +13,12 @@ use crate::sim::spawn::{fall_damage, find_safe_spawn};
|
|||
use crate::sim::visibility::compute_visible_chunks;
|
||||
use crate::sim::{merge_held, step_movement, PlayerBody, SimEvent};
|
||||
use crate::world::{Block, World, WORLD_RADIUS};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{IVec3, Mat4, Vec3};
|
||||
use glam::{IVec3, Vec3};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use wgpu::util::DeviceExt;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{DeviceEvent, ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
|
|
@ -34,32 +31,7 @@ use std::time::Instant;
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
struct CameraUniform {
|
||||
view_proj: [[f32; 4]; 4],
|
||||
inv_view_proj: [[f32; 4]; 4],
|
||||
eye: [f32; 4],
|
||||
/// `.x` = scene time in seconds (drives day/night cycle + leaf sway).
|
||||
/// `.y/.z/.w` reserved. Layout mirrored in `shader.wgsl` as
|
||||
/// `camera.frame` with a `scene_time()` accessor.
|
||||
frame: [f32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
struct OutlineVertex {
|
||||
pos: [f32; 3],
|
||||
}
|
||||
|
||||
struct ChunkBuffers {
|
||||
vertex: wgpu::Buffer,
|
||||
index: wgpu::Buffer,
|
||||
index_count: u32,
|
||||
}
|
||||
|
||||
const REACH: f32 = 6.0;
|
||||
const OUTLINE_VERT_COUNT: u64 = 24;
|
||||
|
||||
thread_local! {
|
||||
/// Storage for the touch/controller bridge. The struct itself lives
|
||||
|
|
@ -251,764 +223,6 @@ mod wasm_api {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
sky_pipeline: wgpu::RenderPipeline,
|
||||
outline_pipeline: wgpu::RenderPipeline,
|
||||
outline_buffer: wgpu::Buffer,
|
||||
depth_view: wgpu::TextureView,
|
||||
camera_buffer: wgpu::Buffer,
|
||||
camera_bind_group: wgpu::BindGroup,
|
||||
window: Arc<Window>,
|
||||
chunk_buffers: HashMap<IVec3, ChunkBuffers>,
|
||||
outline_target: Option<IVec3>,
|
||||
visible_chunks: Vec<IVec3>,
|
||||
remote_vb: wgpu::Buffer,
|
||||
remote_ib: wgpu::Buffer,
|
||||
remote_index_count: u32,
|
||||
|
||||
// ---- Post processing (Step 1: pass-through scene → surface) ----
|
||||
scene_color: wgpu::TextureView,
|
||||
scene_color_format: wgpu::TextureFormat,
|
||||
post_sampler: wgpu::Sampler,
|
||||
post_bgl: wgpu::BindGroupLayout,
|
||||
post_bind_group: wgpu::BindGroup,
|
||||
post_pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
const MAX_REMOTE_PLAYERS: u64 = 32;
|
||||
const REMOTE_VERTS_PER_PLAYER: u64 = 2 * 24;
|
||||
const REMOTE_INDICES_PER_PLAYER: u64 = 2 * 36;
|
||||
|
||||
impl Renderer {
|
||||
pub async fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
#[allow(unused_mut)]
|
||||
let (mut width, mut height) = (size.width.max(1), size.height.max(1));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if width <= 2 || height <= 2 {
|
||||
if let Some(c) = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| d.get_element_by_id("game-canvas"))
|
||||
.and_then(|e| {
|
||||
wasm_bindgen::JsCast::dyn_into::<web_sys::HtmlCanvasElement>(e).ok()
|
||||
})
|
||||
{
|
||||
let w = c.width().max(1);
|
||||
let h = c.height().max(1);
|
||||
log::info!("overriding inner_size {}x{} with canvas {}x{}", width, height, w, h);
|
||||
width = w;
|
||||
height = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("initial surface size: {}x{}", width, height);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let backends = {
|
||||
let webgpu_ok = detect_webgpu().await;
|
||||
if webgpu_ok {
|
||||
log::info!("WebGPU adapter probe OK — using WebGPU backend");
|
||||
wgpu::Backends::BROWSER_WEBGPU
|
||||
} else {
|
||||
log::info!("WebGPU unavailable — using WebGL2 backend");
|
||||
wgpu::Backends::GL
|
||||
}
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let backends = wgpu::Backends::PRIMARY | wgpu::Backends::GL;
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = match instance.create_surface(window.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("create_surface failed: {e:?}");
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
show_browser_compat_error(&format!("{e}"));
|
||||
panic!("could not create rendering surface: {e:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let adapter = match instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Some(a) => a,
|
||||
None => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
show_browser_compat_error(
|
||||
"No GPU adapter available. WebGL2/WebGPU may be disabled in your browser.",
|
||||
);
|
||||
panic!("no suitable GPU adapter");
|
||||
}
|
||||
};
|
||||
|
||||
let info = adapter.get_info();
|
||||
log::info!(
|
||||
"wgpu adapter: backend={:?} type={:?} name={:?}",
|
||||
info.backend,
|
||||
info.device_type,
|
||||
info.name,
|
||||
);
|
||||
let primary_limits = if matches!(info.backend, wgpu::Backend::BrowserWebGpu) {
|
||||
wgpu::Limits::default()
|
||||
} else {
|
||||
wgpu::Limits::downlevel_webgl2_defaults()
|
||||
};
|
||||
|
||||
let device_desc = wgpu::DeviceDescriptor {
|
||||
label: Some("device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: primary_limits,
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
};
|
||||
let (device, queue) = match adapter.request_device(&device_desc, None).await {
|
||||
Ok(dq) => dq,
|
||||
Err(e) => {
|
||||
log::warn!("device request failed ({e:?}); retrying with downlevel limits");
|
||||
let fallback = wgpu::DeviceDescriptor {
|
||||
label: Some("device-fallback"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
};
|
||||
adapter
|
||||
.request_device(&fallback, None)
|
||||
.await
|
||||
.expect("device request failed even with downlevel limits")
|
||||
}
|
||||
};
|
||||
|
||||
let caps = surface.get_capabilities(&adapter);
|
||||
let format = caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(caps.formats[0]);
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let depth_view = create_depth_view(&device, width, height);
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
crate::shader_source::terrain_shader_source().into(),
|
||||
),
|
||||
});
|
||||
|
||||
let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("camera"),
|
||||
contents: bytemuck::bytes_of(&CameraUniform {
|
||||
view_proj: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
inv_view_proj: Mat4::IDENTITY.to_cols_array_2d(),
|
||||
eye: [0.0; 4],
|
||||
frame: [0.0; 4],
|
||||
}),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("camera bgl"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("camera bg"),
|
||||
layout: &camera_bgl,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: camera_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("pl"),
|
||||
bind_group_layouts: &[&camera_bgl],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let sky_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sky pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_sky"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_sky"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let outline_vertex_layout = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<OutlineVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
|
||||
};
|
||||
|
||||
let outline_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("outline buf"),
|
||||
size: OUTLINE_VERT_COUNT * std::mem::size_of::<OutlineVertex>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let remote_vb = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("remote vb"),
|
||||
size: MAX_REMOTE_PLAYERS * REMOTE_VERTS_PER_PLAYER
|
||||
* std::mem::size_of::<Vertex>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let remote_ib = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("remote ib"),
|
||||
size: MAX_REMOTE_PLAYERS * REMOTE_INDICES_PER_PLAYER * 4,
|
||||
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let outline_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("outline pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_outline"),
|
||||
buffers: &[outline_vertex_layout],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_outline"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::LineList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Vertex::LAYOUT],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
// ---------- Post pipeline (Step 1: pass-through) ----------
|
||||
let scene_color_format = config.format;
|
||||
let scene_color = create_scene_color_view(&device, width, height, scene_color_format);
|
||||
|
||||
let post_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("post sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let post_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("post bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let post_bind_group = create_post_bg(&device, &post_bgl, &scene_color, &post_sampler);
|
||||
|
||||
let post_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("post shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(crate::shader_source::post_shader_source().into()),
|
||||
});
|
||||
|
||||
let post_pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("post pl"),
|
||||
bind_group_layouts: &[&post_bgl],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let post_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("post pipeline"),
|
||||
layout: Some(&post_pl),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &post_shader,
|
||||
entry_point: Some("vs_post"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &post_shader,
|
||||
entry_point: Some("fs_post"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
pipeline,
|
||||
sky_pipeline,
|
||||
outline_pipeline,
|
||||
outline_buffer,
|
||||
depth_view,
|
||||
camera_buffer,
|
||||
camera_bind_group,
|
||||
window,
|
||||
chunk_buffers: HashMap::new(),
|
||||
outline_target: None,
|
||||
visible_chunks: Vec::new(),
|
||||
remote_vb,
|
||||
remote_ib,
|
||||
remote_index_count: 0,
|
||||
scene_color,
|
||||
scene_color_format,
|
||||
post_sampler,
|
||||
post_bgl,
|
||||
post_bind_group,
|
||||
post_pipeline,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_remote_players(&mut self, players: &[RemotePlayer]) {
|
||||
if players.is_empty() {
|
||||
self.remote_index_count = 0;
|
||||
return;
|
||||
}
|
||||
let mut verts: Vec<Vertex> = Vec::with_capacity(players.len() * 48);
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(players.len() * 72);
|
||||
let max = players.len().min(MAX_REMOTE_PLAYERS as usize);
|
||||
for p in &players[..max] {
|
||||
let h = name_hash(&p.name);
|
||||
let r = 0.35 + (h & 0x3F) as f32 / 255.0;
|
||||
let g = 0.35 + ((h >> 8) & 0x3F) as f32 / 255.0;
|
||||
let b = 0.35 + ((h >> 16) & 0x3F) as f32 / 255.0;
|
||||
let body = [r, g, b];
|
||||
let head = [r * 0.85 + 0.15, g * 0.85 + 0.15, b * 0.85 + 0.15];
|
||||
emit_oriented_box(
|
||||
Vec3::new(p.pos.x, p.pos.y + 0.65, p.pos.z),
|
||||
Vec3::new(0.3, 0.65, 0.2),
|
||||
p.yaw,
|
||||
body,
|
||||
&mut verts,
|
||||
&mut indices,
|
||||
);
|
||||
emit_oriented_box(
|
||||
Vec3::new(p.pos.x, p.pos.y + 1.55, p.pos.z),
|
||||
Vec3::new(0.25, 0.25, 0.25),
|
||||
p.yaw,
|
||||
head,
|
||||
&mut verts,
|
||||
&mut indices,
|
||||
);
|
||||
}
|
||||
self.remote_index_count = indices.len() as u32;
|
||||
self.queue
|
||||
.write_buffer(&self.remote_vb, 0, bytemuck::cast_slice(&verts));
|
||||
self.queue
|
||||
.write_buffer(&self.remote_ib, 0, bytemuck::cast_slice(&indices));
|
||||
}
|
||||
|
||||
pub fn set_outline(&mut self, target: Option<IVec3>) {
|
||||
if self.outline_target == target {
|
||||
return;
|
||||
}
|
||||
self.outline_target = target;
|
||||
if let Some(b) = target {
|
||||
let eps = 0.002;
|
||||
let min = [b.x as f32 - eps, b.y as f32 - eps, b.z as f32 - eps];
|
||||
let max = [
|
||||
b.x as f32 + 1.0 + eps,
|
||||
b.y as f32 + 1.0 + eps,
|
||||
b.z as f32 + 1.0 + eps,
|
||||
];
|
||||
let c000 = [min[0], min[1], min[2]];
|
||||
let c100 = [max[0], min[1], min[2]];
|
||||
let c001 = [min[0], min[1], max[2]];
|
||||
let c101 = [max[0], min[1], max[2]];
|
||||
let c010 = [min[0], max[1], min[2]];
|
||||
let c110 = [max[0], max[1], min[2]];
|
||||
let c011 = [min[0], max[1], max[2]];
|
||||
let c111 = [max[0], max[1], max[2]];
|
||||
let verts = [
|
||||
c000, c100, c100, c101, c101, c001, c001, c000, c010, c110, c110, c111, c111,
|
||||
c011, c011, c010, c000, c010, c100, c110, c101, c111, c001, c011,
|
||||
];
|
||||
let data: Vec<OutlineVertex> = verts.iter().map(|p| OutlineVertex { pos: *p }).collect();
|
||||
self.queue
|
||||
.write_buffer(&self.outline_buffer, 0, bytemuck::cast_slice(&data));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
if width == 0 || height == 0 {
|
||||
return;
|
||||
}
|
||||
self.config.width = width;
|
||||
self.config.height = height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.depth_view = create_depth_view(&self.device, width, height);
|
||||
self.scene_color =
|
||||
create_scene_color_view(&self.device, width, height, self.scene_color_format);
|
||||
self.post_bind_group =
|
||||
create_post_bg(&self.device, &self.post_bgl, &self.scene_color, &self.post_sampler);
|
||||
}
|
||||
|
||||
pub fn rebuild_chunk(&mut self, coord: IVec3, world: &World) {
|
||||
let Some(chunk) = world.chunks.get(&coord) else {
|
||||
return;
|
||||
};
|
||||
let mesh = build_chunk_mesh(world, chunk);
|
||||
if mesh.indices.is_empty() {
|
||||
self.chunk_buffers.remove(&coord);
|
||||
return;
|
||||
}
|
||||
let vertex = self
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("chunk vb"),
|
||||
contents: bytemuck::cast_slice(&mesh.vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let index = self
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("chunk ib"),
|
||||
contents: bytemuck::cast_slice(&mesh.indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
self.chunk_buffers.insert(
|
||||
coord,
|
||||
ChunkBuffers {
|
||||
vertex,
|
||||
index,
|
||||
index_count: mesh.indices.len() as u32,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn upload_camera(&self, camera: &Camera, time: f32) {
|
||||
let vp = camera.view_proj();
|
||||
let inv = vp.inverse();
|
||||
let uni = CameraUniform {
|
||||
view_proj: vp.to_cols_array_2d(),
|
||||
inv_view_proj: inv.to_cols_array_2d(),
|
||||
eye: [camera.position.x, camera.position.y, camera.position.z, 1.0],
|
||||
frame: [time, 0.0, 0.0, 0.0],
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(&uni));
|
||||
}
|
||||
|
||||
pub fn set_visible(&mut self, chunks: Vec<IVec3>) {
|
||||
self.visible_chunks = chunks;
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Result<(), wgpu::SurfaceError> {
|
||||
let frame = self.surface.get_current_texture()?;
|
||||
let surface_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("enc") });
|
||||
|
||||
// ---- Scene pass: render world into the offscreen scene_color. ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("scene pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.scene_color,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.30,
|
||||
g: 0.55,
|
||||
b: 0.88,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.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,
|
||||
});
|
||||
pass.set_pipeline(&self.sky_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
let iter: Box<dyn Iterator<Item = &ChunkBuffers>> = if self.visible_chunks.is_empty() {
|
||||
Box::new(self.chunk_buffers.values())
|
||||
} else {
|
||||
Box::new(self.visible_chunks.iter().filter_map(|c| self.chunk_buffers.get(c)))
|
||||
};
|
||||
for buf in iter {
|
||||
pass.set_vertex_buffer(0, buf.vertex.slice(..));
|
||||
pass.set_index_buffer(buf.index.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..buf.index_count, 0, 0..1);
|
||||
}
|
||||
if self.remote_index_count > 0 {
|
||||
pass.set_vertex_buffer(0, self.remote_vb.slice(..));
|
||||
pass.set_index_buffer(self.remote_ib.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..self.remote_index_count, 0, 0..1);
|
||||
}
|
||||
if self.outline_target.is_some() {
|
||||
pass.set_pipeline(&self.outline_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.outline_buffer.slice(..));
|
||||
pass.draw(0..OUTLINE_VERT_COUNT as u32, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Post pass: copy scene_color to surface (effects later). ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("post pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &surface_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.post_pipeline);
|
||||
pass.set_bind_group(0, &self.post_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_depth_view(device: &wgpu::Device, w: u32, h: u32) -> wgpu::TextureView {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("depth"),
|
||||
size: wgpu::Extent3d {
|
||||
width: w.max(1),
|
||||
height: h.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
fn create_scene_color_view(
|
||||
device: &wgpu::Device,
|
||||
w: u32,
|
||||
h: u32,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> wgpu::TextureView {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("scene color"),
|
||||
size: wgpu::Extent3d {
|
||||
width: w.max(1),
|
||||
height: h.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
fn create_post_bg(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
scene: &wgpu::TextureView,
|
||||
sampler: &wgpu::Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("post bg"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(scene),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
|
|
@ -1654,97 +868,3 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn detect_webgpu() -> bool {
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
let Some(win) = web_sys::window() else {
|
||||
log::info!("detect_webgpu: no window");
|
||||
return false;
|
||||
};
|
||||
let nav: JsValue = win.navigator().into();
|
||||
let gpu = match js_sys::Reflect::get(&nav, &JsValue::from_str("gpu")) {
|
||||
Ok(v) if !v.is_undefined() && !v.is_null() => v,
|
||||
_ => {
|
||||
log::info!("detect_webgpu: navigator.gpu missing");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let req = match js_sys::Reflect::get(&gpu, &JsValue::from_str("requestAdapter")) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter prop missing");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let req_fn: js_sys::Function = match req.dyn_into() {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter not callable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let promise = match req_fn.call0(&gpu) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::info!("detect_webgpu: requestAdapter threw: {e:?}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let promise: js_sys::Promise = match promise.dyn_into() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
log::info!("detect_webgpu: requestAdapter did not return Promise");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
match wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||
Ok(adapter) => {
|
||||
let ok = !adapter.is_null() && !adapter.is_undefined();
|
||||
log::info!("detect_webgpu: requestAdapter resolved (adapter present = {ok})");
|
||||
ok
|
||||
}
|
||||
Err(e) => {
|
||||
log::info!("detect_webgpu: requestAdapter rejected: {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn show_browser_compat_error(detail: &str) {
|
||||
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
||||
if let Some(body) = doc.body() {
|
||||
let html = format!(
|
||||
"<div id=\"compat-error\" style=\"\
|
||||
position:fixed; inset:0; background:rgba(0,0,0,0.85);\
|
||||
color:#f0f0f0; padding:40px; font-family:system-ui,sans-serif;\
|
||||
z-index:1000; overflow:auto;\">\
|
||||
<h1 style=\"margin-top:0;color:#f88;\">Couldn't start the renderer</h1>\
|
||||
<p>Your browser couldn't open a WebGL2 or WebGPU context on the canvas.</p>\
|
||||
<p><strong>Chrome / Chromium / Edge:</strong> open \
|
||||
<code>chrome://gpu</code> and confirm \"WebGL 2\" says \
|
||||
<em>Hardware accelerated</em>. If not, enable hardware acceleration in \
|
||||
<code>chrome://settings/system</code>, or set \
|
||||
<code>chrome://flags/#ignore-gpu-blocklist</code> to <em>Enabled</em>.\
|
||||
</p>\
|
||||
<p><strong>LibreWolf / Firefox:</strong> in <code>about:config</code> set \
|
||||
<code>webgl.disabled</code> = false, \
|
||||
<code>webgl.force-enabled</code> = true, \
|
||||
<code>privacy.resistFingerprinting</code> = false.\
|
||||
</p>\
|
||||
<p style=\"opacity:0.65;font-size:12px;\">Underlying error: {detail}</p>\
|
||||
</div>",
|
||||
detail = html_escape(detail)
|
||||
);
|
||||
let _ = body.insert_adjacent_html("beforeend", &html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn html_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue