separate out renderer crate (#13)
* add keyboard handler * some cleanup * add l key binding * Fix WASM shared memory generation for Rust nightly 2025-10-20+ Add explicit linker flags for shared memory generation after Rust PR #147225 removed automatic flags with +atomics. This fixes DataCloneError when sharing memory with workers. Changes: - Add --shared-memory, --max-memory, --import-memory flags - Export TLS symbols: __wasm_init_tls, __tls_size, __tls_align, __tls_base - Update both Cargo.toml and package.json build scripts Amp-Thread-ID: https://ampcode.com/threads/T-41d99b69-5754-4fdf-b06e-a46343fa7485 Co-authored-by: Amp <amp@ampcode.com> * add a level-editor crate * remove redundant worker setup * simplify app setup * add SceneBuilder * separate out default scene implementation in a crate * define default method for MeshBuilder --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use ultraviolet::Mat4;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use renderer::app_setup::WebApp;
|
||||
use renderer::camera::Camera;
|
||||
use renderer::message::WindowEvent;
|
||||
use renderer::renderer as gpu_renderer;
|
||||
use renderer::renderer::scene::{mesh_vertex_layout, FrameMetadata, Mesh, MeshBuilder};
|
||||
|
||||
/// Simple vertex format.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Vertex {
|
||||
pos: [f32; 3],
|
||||
color: [f32; 3],
|
||||
}
|
||||
|
||||
pub struct EditorScene {
|
||||
uniform_buffers: [wgpu::Buffer; 2],
|
||||
bind_groups: [wgpu::BindGroup; 2],
|
||||
bind_group_layouts: [wgpu::BindGroupLayout; 2],
|
||||
frame_metadata: FrameMetadata,
|
||||
cam: Camera,
|
||||
meshes: Vec<Mesh>,
|
||||
}
|
||||
|
||||
impl renderer::renderer::scene::Scene for EditorScene {
|
||||
fn setup(
|
||||
renderer_context: &gpu_renderer::RendererContext,
|
||||
resources: &mut gpu_renderer::GpuResources,
|
||||
) -> Self {
|
||||
let dimension = ultraviolet::Vec2::new(
|
||||
renderer_context.surface_config.width as f32,
|
||||
renderer_context.surface_config.height as f32,
|
||||
);
|
||||
|
||||
let mut frame_metadata = FrameMetadata::new(dimension);
|
||||
let camera = Camera::new(dimension.x / dimension.y);
|
||||
|
||||
frame_metadata.set_camera_position(camera.position());
|
||||
|
||||
let uniform_resource = frame_metadata.create_uniform_resource(&renderer_context.device);
|
||||
let camera_resource = camera.create_uniform_resource(&renderer_context.device);
|
||||
|
||||
let bind_group_layouts = [
|
||||
uniform_resource.bind_group_layout,
|
||||
camera_resource.bind_group_layout,
|
||||
];
|
||||
|
||||
resources.set_bind_group_layouts(&bind_group_layouts);
|
||||
|
||||
let mut scene = EditorScene {
|
||||
uniform_buffers: [uniform_resource.buffer, camera_resource.buffer],
|
||||
bind_groups: [uniform_resource.bind_group, camera_resource.bind_group],
|
||||
bind_group_layouts,
|
||||
frame_metadata,
|
||||
cam: camera,
|
||||
meshes: Vec::new(),
|
||||
};
|
||||
|
||||
scene.create_default_scene(
|
||||
&renderer_context.device,
|
||||
resources,
|
||||
renderer_context.surface_config.format,
|
||||
);
|
||||
|
||||
scene
|
||||
}
|
||||
|
||||
fn frame_metadata_mut(&mut self) -> Option<&mut FrameMetadata> {
|
||||
Some(&mut self.frame_metadata)
|
||||
}
|
||||
|
||||
fn camera_mut(&mut self) -> Option<&mut Camera> {
|
||||
Some(&mut self.cam)
|
||||
}
|
||||
|
||||
fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
|
||||
Some(&self.uniform_buffers)
|
||||
}
|
||||
|
||||
fn bind_groups(&self) -> &[wgpu::BindGroup] {
|
||||
&self.bind_groups
|
||||
}
|
||||
|
||||
fn meshes(&self) -> &[Mesh] {
|
||||
&self.meshes
|
||||
}
|
||||
|
||||
fn handle_mouse_click(&mut self, x: f32, y: f32) {
|
||||
self.frame_metadata.mouse_click = [x, y];
|
||||
}
|
||||
|
||||
fn handle_zoom(&mut self, _delta_y: f32) {
|
||||
// TODO: Implement zoom properly when Camera exposes necessary methods
|
||||
}
|
||||
|
||||
fn handle_orbit(&mut self, delta_x: f32, delta_y: f32) {
|
||||
self.cam.orbit(delta_x, delta_y);
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.meshes.clear();
|
||||
}
|
||||
|
||||
fn add_mesh(&mut self, mesh: Mesh) {
|
||||
self.meshes.push(mesh);
|
||||
}
|
||||
|
||||
fn set_camera_depth_range(&mut self, near: f32, far: f32) {
|
||||
self.cam.set_depth_range(near, far);
|
||||
}
|
||||
|
||||
fn set_camera_look_at(&mut self, eye: ultraviolet::Vec3, center: ultraviolet::Vec3) {
|
||||
self.cam.look_at(eye, center);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct LevelEditor {
|
||||
#[allow(dead_code)]
|
||||
scene: EditorScene,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WebApp for LevelEditor {
|
||||
type Scene = EditorScene;
|
||||
}
|
||||
|
||||
impl EditorScene {
|
||||
/// Ground plane vertex data.
|
||||
const VERTICES: &[Vertex] = &[
|
||||
// First triangle of quad
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
// Second triangle of quad
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, -5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
Vertex {
|
||||
pos: [-5.0, 0.0, 5.0],
|
||||
color: [0.2, 0.8, 0.2], // Green
|
||||
},
|
||||
];
|
||||
// Wind the ground plane so the upward-facing side is front-facing (CCW from
|
||||
// above) to avoid being culled by the default back-face culling.
|
||||
const INDICES: &[u32] = &[0, 2, 1, 3, 5, 4];
|
||||
|
||||
fn create_default_scene(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
resources: &mut gpu_renderer::GpuResources,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
) {
|
||||
let positions: Vec<[f32; 3]> = Self::VERTICES.iter().map(|v| v.pos).collect();
|
||||
// Ground plane normals point upward (Y+)
|
||||
let normals: Vec<[f32; 3]> = vec![[0.0, 1.0, 0.0]; positions.len()];
|
||||
let uvs: &[[f32; 2]] = &[
|
||||
[0.0, 0.0],
|
||||
[1.0, 0.0],
|
||||
[0.0, 1.0],
|
||||
[1.0, 0.0],
|
||||
[1.0, 1.0],
|
||||
[0.0, 1.0],
|
||||
];
|
||||
|
||||
let vertex_layout = mesh_vertex_layout();
|
||||
|
||||
let pipeline_index = resources.get_or_create_pipeline(
|
||||
device,
|
||||
"ground_plane",
|
||||
&vertex_layout,
|
||||
include_str!("./program.wgsl"),
|
||||
surface_format,
|
||||
);
|
||||
|
||||
let scale_factor = 100.0;
|
||||
let scale_matrix = Mat4::from_scale(scale_factor);
|
||||
|
||||
let mesh = MeshBuilder::default()
|
||||
.with_vertices(device, resources, &positions, &normals, uvs)
|
||||
.with_indices(device, resources, Self::INDICES)
|
||||
.with_pipeline(pipeline_index)
|
||||
.with_model_matrix(device, resources, scale_matrix)
|
||||
.build();
|
||||
|
||||
self.meshes.push(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entrypoint for the level editor
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let runtime = LevelEditor::setup_runtime().unwrap();
|
||||
// Keep the runtime running and prevent drops
|
||||
Box::leak(Box::new(runtime));
|
||||
});
|
||||
}
|
||||
|
||||
renderer::export_worker_entrypoint!();
|
||||
@@ -0,0 +1,68 @@
|
||||
struct UniformData {
|
||||
mouse_move: vec2<f32>,
|
||||
mouse_click: vec2<f32>,
|
||||
resolution: vec2<f32>,
|
||||
time: f32,
|
||||
_padding0: f32,
|
||||
camera_position: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uni: UniformData;
|
||||
@group(1) @binding(0) var<uniform> view_proj: mat4x4<f32>;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) model_col0: vec4<f32>,
|
||||
@location(4) model_col1: vec4<f32>,
|
||||
@location(5) model_col2: vec4<f32>,
|
||||
@location(6) model_col3: vec4<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_pos: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>
|
||||
}
|
||||
|
||||
|
||||
@vertex
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let model = mat4x4<f32>(
|
||||
in.model_col0,
|
||||
in.model_col1,
|
||||
in.model_col2,
|
||||
in.model_col3,
|
||||
);
|
||||
let world_position = model * vec4<f32>(in.pos, 1.0);
|
||||
out.clip_position = view_proj * world_position;
|
||||
out.world_pos = world_position.xyz;
|
||||
out.normal = normalize(in.normal);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
|
||||
let light_color = vec3<f32>(1.0, 0.95, 0.85);
|
||||
let base_color = vec3<f32>(0.2, 0.2, 0.2);
|
||||
|
||||
let normal = normalize(in.normal);
|
||||
let view_dir = normalize(uni.camera_position.xyz - in.world_pos);
|
||||
|
||||
let diffuse_strength = max(dot(normal, light_direction), 0.0);
|
||||
let ambient = 0.15;
|
||||
|
||||
var specular = 0.0;
|
||||
if diffuse_strength > 0.0 {
|
||||
let halfway_dir = normalize(light_direction + view_dir);
|
||||
specular = pow(max(dot(normal, halfway_dir), 0.0), 32.0);
|
||||
}
|
||||
|
||||
let lighting = min(base_color * (ambient + diffuse_strength) + light_color * specular, vec3<f32>(1.0));
|
||||
let x = select(0.0, 0.3, distance(in.clip_position.xy, uni.mouse_move) < 25.0);
|
||||
let y = select(0.0, 0.3, distance(in.clip_position.xy, uni.mouse_click) < 25.0);
|
||||
return vec4<f32>(lighting + x - y, 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user