pub(crate) const GAME_3D_CANVAS_ID: &str = "game-3d-canvas";
pub(crate) const GAME_3D_CANVAS_SELECTOR: &str = "#game-3d-canvas";
pub(crate) const GAME_3D_CANVAS_WIDTH: f64 = 600.0;
pub(crate) const GAME_3D_CANVAS_HEIGHT: f64 = 400.0;
pub(crate) const GAME_3D_FIXED_TIMESTEP: f64 = 1.0 / 60.0;
pub(crate) const GAME_3D_CUBE_HALF_SIZE: f64 = 1.0;
pub(crate) const GAME_3D_CAMERA_DISTANCE: f64 = 8.0;
pub(crate) const GAME_3D_AUTO_YAW_SPEED: f64 = 0.5;
pub(crate) const GAME_3D_PITCH_CLAMP: f64 = 0.01;
pub(crate) const GAME_3D_RESIZE_DEBOUNCE_MILLIS: i32 = 100;
pub(crate) const GAME_3D_LOOP_START_DELAY_MILLIS: i32 = 360;
pub(crate) const GAME_3D_PROPERTY_FILL_STYLE: &str = "fillStyle";
pub(crate) const GAME_3D_PROPERTY_BACKGROUND_COLOR: &str = "background-color";
pub(crate) const GAME_3D_LOADING_TEXT: &str = "Loading...";
pub(crate) const GAME_3D_LOADING_FONT_FAMILY: &str = "sans-serif";
pub(crate) const GAME_3D_LOADING_FONT_SIZE_RATIO: f64 = 0.04;
pub(crate) const GAME_3D_LOADING_COLOR_VAR: &str = "--text-on-accent";
pub(crate) const GAME_3D_LOADING_MIN_MILLIS: i32 = 400;
pub(crate) const GAME_3D_PROPERTY_STROKE_STYLE: &str = "strokeStyle";
pub(crate) const GAME_3D_CUBE_FACE_COLOR: &str = "#16c79a";
pub(crate) const GAME_3D_CUBE_EDGE_COLOR: &str = "#e94560";
pub(crate) const GAME_3D_EVENT_PROPERTY_TOUCHES: &str = "touches";
pub(crate) const GAME_3D_EVENT_PROPERTY_CLIENT_X: &str = "clientX";
pub(crate) const GAME_3D_EVENT_PROPERTY_CLIENT_Y: &str = "clientY";
pub(crate) const GAME_3D_EVENT_WHEEL: &str = "wheel";
pub(crate) const GAME_3D_EVENT_TOUCH_START: &str = "touchstart";
pub(crate) const GAME_3D_EVENT_TOUCH_MOVE: &str = "touchmove";
pub(crate) const GAME_3D_CUBE_VERTICES: [(f64, f64, f64); 8] = [
(-1.0, -1.0, -1.0),
(1.0, -1.0, -1.0),
(1.0, 1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, -1.0, 1.0),
(1.0, -1.0, 1.0),
(1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0),
];
pub(crate) const GAME_3D_CUBE_FACES: [(usize, usize, usize, usize); 6] = [
(0, 3, 2, 1),
(4, 5, 6, 7),
(0, 1, 5, 4),
(2, 3, 7, 6),
(0, 4, 7, 3),
(1, 2, 6, 5),
];
pub(crate) const GAME_3D_CUBE_EDGES: [(usize, usize); 12] = [
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
];
pub(crate) const GAME_3D_WEBGPU_CANVAS_ID: &str = "game-3d-webgpu-canvas";
pub(crate) const GAME_3D_WEBGPU_CANVAS_SELECTOR: &str = "#game-3d-webgpu-canvas";
pub(crate) const GAME_3D_WEBGPU_LOADING_CANVAS_ID: &str = "game-3d-webgpu-loading-canvas";
pub(crate) const GAME_3D_WEBGPU_LOADING_CANVAS_SELECTOR: &str = "#game-3d-webgpu-loading-canvas";
pub(crate) const GAME_3D_GPU_MAX_CUBES: usize = 8;
pub(crate) const GAME_3D_WEBGPU_SHADER: &str = r#"
struct CubeData {
rotation: vec4<f32>,
pos_scale: vec4<f32>,
face_color: vec4<f32>,
edge_color: vec4<f32>,
};
struct SceneUniforms {
view_proj: mat4x4<f32>,
camera_pos: vec4<f32>,
cubes: array<CubeData, 8>,
};
@group(0) @binding(0) var<uniform> u_scene: SceneUniforms;
const CUBE_CORNERS = array<vec3<f32>, 8>(
vec3<f32>(-1.0, -1.0, -1.0),
vec3<f32>(1.0, -1.0, -1.0),
vec3<f32>(1.0, 1.0, -1.0),
vec3<f32>(-1.0, 1.0, -1.0),
vec3<f32>(-1.0, -1.0, 1.0),
vec3<f32>(1.0, -1.0, 1.0),
vec3<f32>(1.0, 1.0, 1.0),
vec3<f32>(-1.0, 1.0, 1.0),
);
const FACE_INDICES = array<u32, 24>(
0u, 3u, 2u, 1u,
4u, 5u, 6u, 7u,
0u, 1u, 5u, 4u,
2u, 3u, 7u, 6u,
0u, 4u, 7u, 3u,
1u, 2u, 6u, 5u,
);
const FACE_NORMALS = array<vec3<f32>, 6>(
vec3<f32>(0.0, 0.0, -1.0),
vec3<f32>(0.0, 0.0, 1.0),
vec3<f32>(0.0, -1.0, 0.0),
vec3<f32>(0.0, 1.0, 0.0),
vec3<f32>(-1.0, 0.0, 0.0),
vec3<f32>(1.0, 0.0, 0.0),
);
const CORNER_MAP = array<u32, 6>(0u, 1u, 2u, 0u, 2u, 3u);
const CORNER_UVS = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 1.0),
);
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) normal: vec3<f32>,
@location(2) world_pos: vec3<f32>,
@location(3) face_color: vec3<f32>,
@location(4) edge_color: vec3<f32>,
};
/// Rotates vector `v` by unit quaternion `q`.
/// Helper body of the `quat_rotate` free function.
///
/// # Arguments
///
/// - `vec4<f32>` - A `vec4<f32>` parameter.
/// - `vec3<f32>` - A `vec3<f32>` parameter.
///
/// # Returns
///
/// - `vec3<f32>` - A `vec3<f32>` value.
fn quat_rotate(q: vec4<f32>, v: vec3<f32>) -> vec3<f32> {
return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
}
@vertex
/// WGSL vertex shader entry point.
/// Helper body of the `vs_main` free function.
///
/// # Arguments
///
/// - `u32` - A 32-bit unsigned integer (`u32`).
///
/// # Returns
///
/// - `VertexOutput` - A `VertexOutput` value.
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
let cube = u_scene.cubes[vi / 36u];
let face_index = (vi % 36u) / 6u;
let corner_index = vi % 6u;
let local = CUBE_CORNERS[FACE_INDICES[face_index * 4u + CORNER_MAP[corner_index]]]
* cube.pos_scale.w;
let world = quat_rotate(cube.rotation, local) + cube.pos_scale.xyz;
let clip = u_scene.view_proj * vec4<f32>(world, 1.0);
var out: VertexOutput;
// The host view-projection matrix produces OpenGL-style clip z in
// [-1, 1]; remap to WebGPU's [0, 1] range.
out.position = vec4<f32>(clip.x, clip.y, clip.z * 0.5 + clip.w * 0.5, clip.w);
out.uv = CORNER_UVS[corner_index];
out.normal = quat_rotate(cube.rotation, FACE_NORMALS[face_index]);
out.world_pos = world;
out.face_color = cube.face_color.rgb;
out.edge_color = cube.edge_color.rgb;
return out;
}
@fragment
/// WGSL fragment shader entry point.
/// Helper body of the `fs_main` free function.
///
/// # Arguments
///
/// - `VertexOutput` - A `VertexOutput` parameter.
///
/// # Returns
///
/// - `@location(0) vec4<f32>` - A `@location(0) vec4<f32>` value.
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let view_dir = normalize(in.world_pos - u_scene.camera_pos.xyz);
if dot(in.normal, view_dir) >= 0.0 {
discard;
}
// Edge overlay: distance to the quad border in screen pixels, so the
// wireframe tracks the Canvas 2D stroke width at any zoom level.
let fw = fwidth(in.uv);
let dist_x = min(in.uv.x, 1.0 - in.uv.x) / max(fw.x, 1e-6);
let dist_y = min(in.uv.y, 1.0 - in.uv.y) / max(fw.y, 1e-6);
if min(dist_x, dist_y) < 0.75 {
return vec4<f32>(in.edge_color, 1.0);
}
return vec4<f32>(in.face_color, 1.0);
}
"#;
pub(crate) const GAME_3D_WEBGL_CANVAS_ID: &str = "game-3d-webgl-canvas";
pub(crate) const GAME_3D_WEBGL_CANVAS_SELECTOR: &str = "#game-3d-webgl-canvas";
pub(crate) const GAME_3D_WEBGL_LOADING_CANVAS_ID: &str = "game-3d-webgl-loading-canvas";
pub(crate) const GAME_3D_WEBGL_LOADING_CANVAS_SELECTOR: &str = "#game-3d-webgl-loading-canvas";
pub(crate) const GAME_3D_WEBGL_VERTEX_SHADER: &str = r#"#version 300 es
uniform vec4 u_view_proj[4];
uniform vec4 u_camera_pos;
uniform vec4 u_cubes[32];
out vec2 v_uv;
out vec3 v_normal;
out vec3 v_world_pos;
out vec3 v_face_color;
out vec3 v_edge_color;
vec3 quat_rotate(vec4 q, vec3 v) {
return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
}
void main() {
vec3 corners[8] = vec3[8](
vec3(-1.0, -1.0, -1.0),
vec3(1.0, -1.0, -1.0),
vec3(1.0, 1.0, -1.0),
vec3(-1.0, 1.0, -1.0),
vec3(-1.0, -1.0, 1.0),
vec3(1.0, -1.0, 1.0),
vec3(1.0, 1.0, 1.0),
vec3(-1.0, 1.0, 1.0)
);
int face_indices[24] = int[24](
0, 3, 2, 1,
4, 5, 6, 7,
0, 1, 5, 4,
2, 3, 7, 6,
0, 4, 7, 3,
1, 2, 6, 5
);
vec3 face_normals[6] = vec3[6](
vec3(0.0, 0.0, -1.0),
vec3(0.0, 0.0, 1.0),
vec3(0.0, -1.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(-1.0, 0.0, 0.0),
vec3(1.0, 0.0, 0.0)
);
int corner_map[6] = int[6](0, 1, 2, 0, 2, 3);
vec2 corner_uvs[6] = vec2[6](
vec2(0.0, 0.0),
vec2(1.0, 0.0),
vec2(1.0, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 1.0),
vec2(0.0, 1.0)
);
int cube_index = gl_VertexID / 36;
int face_index = (gl_VertexID % 36) / 6;
int corner_index = gl_VertexID % 6;
vec4 rotation = u_cubes[cube_index * 4];
vec4 pos_scale = u_cubes[cube_index * 4 + 1];
vec4 face_color = u_cubes[cube_index * 4 + 2];
vec4 edge_color = u_cubes[cube_index * 4 + 3];
vec3 local = corners[face_indices[face_index * 4 + corner_map[corner_index]]]
* pos_scale.w;
vec3 world = quat_rotate(rotation, local) + pos_scale.xyz;
mat4 view_proj = mat4(u_view_proj[0], u_view_proj[1], u_view_proj[2], u_view_proj[3]);
gl_Position = view_proj * vec4(world, 1.0);
v_uv = corner_uvs[corner_index];
v_normal = quat_rotate(rotation, face_normals[face_index]);
v_world_pos = world;
v_face_color = face_color.rgb;
v_edge_color = edge_color.rgb;
}
"#;
pub(crate) const GAME_3D_WEBGL_FRAGMENT_SHADER: &str = r#"#version 300 es
precision highp float;
uniform vec4 u_camera_pos;
in vec2 v_uv;
in vec3 v_normal;
in vec3 v_world_pos;
in vec3 v_face_color;
in vec3 v_edge_color;
out vec4 out_color;
void main() {
vec3 view_dir = normalize(v_world_pos - u_camera_pos.xyz);
if (dot(v_normal, view_dir) >= 0.0) {
discard;
}
vec2 fw = fwidth(v_uv);
float dist_x = min(v_uv.x, 1.0 - v_uv.x) / max(fw.x, 1e-6);
float dist_y = min(v_uv.y, 1.0 - v_uv.y) / max(fw.y, 1e-6);
if (min(dist_x, dist_y) < 0.75) {
out_color = vec4(v_edge_color, 1.0);
return;
}
out_color = vec4(v_face_color, 1.0);
}
"#;