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:
Akash Shakdwipeea
2025-10-22 18:08:02 +05:30
committed by GitHub
co-authored by Amp
parent cb65a52e95
commit b295c0bf0b
33 changed files with 5516 additions and 995 deletions
+46
View File
@@ -0,0 +1,46 @@
cargo-features = ["profile-rustflags"]
[package]
name = "level-editor"
description = "Level editor application using the renderer"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
default = []
atomics = []
bulk-memory = []
[profile.release]
opt-level = "z"
lto = true
[target.wasm32-unknown-unknown]
rustflags = [
"-Clink-args=--shared-memory",
"-Clink-args=--max-memory=1073741824",
"-Clink-args=--import-memory",
"-Clink-args=--export=__wasm_init_tls",
"-Clink-args=--export=__tls_size",
"-Clink-args=--export=__tls_align",
"-Clink-args=--export=__tls_base",
]
[dependencies]
renderer = { path = "../renderer" }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
console_error_panic_hook = { workspace = true }
log = { workspace = true }
wasm-logger = { workspace = true }
web-sys = { workspace = true }
js-sys = { workspace = true }
ultraviolet = { workspace = true }
wgpu = { workspace = true }
bytemuck = { workspace = true }
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+221
View File
@@ -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!();
+68
View File
@@ -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);
}