mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Single-object viewer POC (#14)
This commit implements a Sketchfab-style single-object viewer as a minimal proof-of-concept. ### `@storyteller/studio-web` This is a new package to house web components for interfacing with the `@storyteller/studio` WASM library. It currently includes a single component, `<sts-viewer />`, which is exported from `@storyteller/studio-web/viewer`. ### `react-frontend` This is a new application intended as a proof-of-concept and testbed for React interop with `@storyteller/studio-web`. Web components are currently a little bit fiddly to use in a React application. `react-frontend/src/app/studio-viewer.tsx` demonstrates how a React-idiomatic component can be written as a thin wrapper around a web component, and `react-frontend/src/app/app.tsx` demonstrates that wrapper component in use.
This commit is contained in:
+25
-1
@@ -7,11 +7,18 @@ use bevy_mod_picking::debug::DebugPickingMode;
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraMotionSystem},
|
||||
input::{Action, InputAction, Lifecycle},
|
||||
MainCamera,
|
||||
};
|
||||
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct DebugMode(bool);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug)]
|
||||
pub struct DebugPoint {
|
||||
pub point: Vec3,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct DebugPlugin;
|
||||
impl Plugin for DebugPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
@@ -31,7 +38,7 @@ impl Plugin for DebugPlugin {
|
||||
toggle_debug_mode,
|
||||
update_debug_picking_mode.run_if(resource_changed::<DebugMode>()),
|
||||
update_gizmo_line_thickness,
|
||||
draw_world_axes
|
||||
(draw_world_axes, draw_debug_points)
|
||||
.after(CameraMotionSystem)
|
||||
.run_if(is_debug_mode),
|
||||
),
|
||||
@@ -84,6 +91,23 @@ fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) {
|
||||
draw_axis(Vec3::Z, Color::BLUE);
|
||||
}
|
||||
|
||||
fn draw_debug_points(
|
||||
mut gizmos: Gizmos,
|
||||
q_debug_points: Query<&DebugPoint>,
|
||||
q_cam_xform: Query<&GlobalTransform, With<MainCamera>>,
|
||||
) {
|
||||
let Ok(cam_xform) = q_cam_xform.get_single() else {
|
||||
return;
|
||||
};
|
||||
for &DebugPoint { point, color } in q_debug_points.iter() {
|
||||
let dist = cam_xform.translation().distance(point);
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(point).with_scale(Vec3::splat(dist / 300.)),
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(dead_code)]
|
||||
pub fn write_mat3(w: &mut impl fmt::Write, m: &Mat3) -> fmt::Result {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm;
|
||||
|
||||
pub struct InspectorPlugin;
|
||||
|
||||
impl Plugin for InspectorPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
wasm::notify_entity_spawn_changes.map(bevy::utils::error),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::{prelude::*, utils::HashSet, window::PrimaryWindow};
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
scene::SceneElement,
|
||||
wasm::{self, AsJson},
|
||||
};
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TYPE_DECLARATIONS: &str = r#"
|
||||
export interface SceneObject {
|
||||
/** The entity ID. */
|
||||
id: string;
|
||||
/** A user-friendly name for displaying in the UI */
|
||||
name: string;
|
||||
/** The IDs of this object's children in the hierarchy */
|
||||
children: string[];
|
||||
/** The ID of this object's parent in the hierarchy, if it has one */
|
||||
parent: string | null;
|
||||
/** The local-space transform */
|
||||
transform: Transform;
|
||||
/** The world-space transform */
|
||||
globalTransform: GlobalTransform;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that a new entity has spawned.
|
||||
*/
|
||||
export interface EntitySpawnEvent extends CustomEvent {
|
||||
type: "entity-spawn";
|
||||
detail: SceneObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that an entity has been de-spawned.
|
||||
*/
|
||||
export interface EntityDespawnEvent extends CustomEvent {
|
||||
type: "entity-despawn";
|
||||
/** The entity ID */
|
||||
detail: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that multiple entities have spawned.
|
||||
*/
|
||||
export interface EntityMultiSpawnEvent extends CustomEvent {
|
||||
type: "entity-multi-spawn";
|
||||
detail: SceneObject[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface HTMLElementEventMap {
|
||||
"entity-spawn": EntitySpawnEvent;
|
||||
"entity-despawn": EntityDespawnEvent;
|
||||
"entity-multi-spawn": EntityMultiSpawnEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct SceneObject {
|
||||
id: String,
|
||||
name: String,
|
||||
children: Vec<String>,
|
||||
parent: Option<String>,
|
||||
transform: serde_json::Value,
|
||||
#[serde(rename(serialize = "globalTransform"))]
|
||||
global_transform: serde_json::Value,
|
||||
}
|
||||
|
||||
struct SceneObjectBuilder<'a> {
|
||||
id: Entity,
|
||||
name: Option<String>,
|
||||
children: Option<Vec<Entity>>,
|
||||
parent: Option<Entity>,
|
||||
transform: Option<&'a Transform>,
|
||||
global_transform: Option<&'a GlobalTransform>,
|
||||
}
|
||||
|
||||
impl<'a> SceneObjectBuilder<'a> {
|
||||
fn new(id: Entity) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: None,
|
||||
children: None,
|
||||
parent: None,
|
||||
transform: None,
|
||||
global_transform: None,
|
||||
}
|
||||
}
|
||||
fn finish(self) -> SceneObject {
|
||||
SceneObject {
|
||||
id: format!("{:?}", self.id),
|
||||
name: self.name.unwrap(),
|
||||
children: self
|
||||
.children
|
||||
.map(|children| children.iter().map(|ent| format!("{ent:?}")).collect())
|
||||
.unwrap_or_default(),
|
||||
parent: self.parent.map(|ent| format!("{ent:?}")),
|
||||
transform: self.transform.unwrap().as_json(),
|
||||
global_transform: self.global_transform.unwrap().as_json(),
|
||||
}
|
||||
}
|
||||
fn with_name(mut self, name: String) -> Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
fn with_children(mut self, children: Vec<Entity>) -> Self {
|
||||
self.children = Some(children);
|
||||
self
|
||||
}
|
||||
fn with_parent(mut self, parent: Entity) -> Self {
|
||||
self.parent = Some(parent);
|
||||
self
|
||||
}
|
||||
fn with_transform(mut self, transform: &'a Transform) -> Self {
|
||||
self.transform = Some(transform);
|
||||
self
|
||||
}
|
||||
fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self {
|
||||
self.global_transform = Some(global_transform);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SceneObject {
|
||||
fn build(id: Entity) -> SceneObjectBuilder<'a> {
|
||||
SceneObjectBuilder::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn notify_entity_spawn_changes(
|
||||
mut l_notified: Local<HashSet<Entity>>,
|
||||
// Note: We use locals for these data structures purely to reuse
|
||||
// existing allocations across multiple invocations of this system
|
||||
mut l_new_objects: Local<Vec<SceneObject>>,
|
||||
mut l_despawned_entities: Local<Vec<Entity>>,
|
||||
mut l_missing_parents: Local<VecDeque<Entity>>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With<SceneElement>>,
|
||||
q_others: Query<(Entity, &Transform, &GlobalTransform), Without<SceneElement>>,
|
||||
q_names: Query<&Name>,
|
||||
q_parents: Query<&Children>,
|
||||
q_children: Query<&Parent>,
|
||||
) -> Result<(), String> {
|
||||
l_missing_parents.clear();
|
||||
|
||||
l_new_objects.clear();
|
||||
l_new_objects.extend(
|
||||
q_inspectables
|
||||
.iter()
|
||||
.filter_map(|(ent, xform, world_xform)| {
|
||||
build_scene_object(
|
||||
&mut l_notified,
|
||||
&mut l_missing_parents,
|
||||
ent,
|
||||
xform,
|
||||
world_xform,
|
||||
&q_inspectables,
|
||||
&q_names,
|
||||
&q_parents,
|
||||
&q_children,
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
while let Some(parent) = l_missing_parents.pop_front() {
|
||||
let Ok((ent, xform, world_xform)) = q_others.get(parent) else {
|
||||
error!("Failed to find missing parent: {parent:?}");
|
||||
l_notified.insert(parent);
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(object) = build_scene_object(
|
||||
&mut l_notified,
|
||||
&mut l_missing_parents,
|
||||
ent,
|
||||
xform,
|
||||
world_xform,
|
||||
&q_inspectables,
|
||||
&q_names,
|
||||
&q_parents,
|
||||
&q_children,
|
||||
) {
|
||||
l_new_objects.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
l_despawned_entities.clear();
|
||||
l_despawned_entities.extend(
|
||||
l_notified
|
||||
.iter()
|
||||
.filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
let win = q_window.single();
|
||||
|
||||
for ent in l_despawned_entities.iter() {
|
||||
// TODO: Make this take an array like the multi-spawn event
|
||||
wasm::dispatch_to_js(win, "entity-despawn", ent)?;
|
||||
l_notified.remove(ent);
|
||||
}
|
||||
|
||||
if !l_new_objects.is_empty() {
|
||||
// TODO: Remove the single-spawn event
|
||||
wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_scene_object(
|
||||
notified: &mut HashSet<Entity>,
|
||||
missing_parents: &mut VecDeque<Entity>,
|
||||
ent: Entity,
|
||||
xform: &Transform,
|
||||
world_xform: &GlobalTransform,
|
||||
q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With<SceneElement>>,
|
||||
q_names: &Query<&Name>,
|
||||
q_parents: &Query<&Children>,
|
||||
q_children: &Query<&Parent>,
|
||||
) -> Option<SceneObject> {
|
||||
if notified.contains(&ent) {
|
||||
return None;
|
||||
}
|
||||
|
||||
notified.insert(ent);
|
||||
|
||||
let mut result = SceneObject::build(ent)
|
||||
.with_transform(xform)
|
||||
.with_global_transform(world_xform);
|
||||
|
||||
if let Ok(children) = q_parents.get(ent) {
|
||||
result = result.with_children(children.iter().cloned().collect());
|
||||
}
|
||||
if let Ok(parent) = q_children.get(ent) {
|
||||
let parent_ent = **parent;
|
||||
result = result.with_parent(parent_ent);
|
||||
|
||||
if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) {
|
||||
missing_parents.push_back(parent_ent);
|
||||
}
|
||||
}
|
||||
|
||||
result = result.with_name(match q_names.get(ent) {
|
||||
Ok(name) => name.into(),
|
||||
Err(_) => format!("{ent:?}"),
|
||||
});
|
||||
|
||||
Some(result.finish())
|
||||
}
|
||||
@@ -2,9 +2,12 @@ use bevy::prelude::*;
|
||||
use bevy_mod_outline::{
|
||||
AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume,
|
||||
};
|
||||
use bevy_mod_picking::{prelude::Pointer, selection::Select};
|
||||
use bevy_mod_picking::{prelude::Pointer, selection::Select, PickableBundle};
|
||||
|
||||
use crate::input::{self, Action, InputAction, Lifecycle};
|
||||
use crate::{
|
||||
input::{self, Action, InputAction, Lifecycle},
|
||||
scene::{SceneElement, SceneSpawnerSystem, SceneState},
|
||||
};
|
||||
|
||||
pub struct SelectionPlugin;
|
||||
impl Plugin for SelectionPlugin {
|
||||
@@ -14,6 +17,12 @@ impl Plugin for SelectionPlugin {
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(apply_deferred, tag_scene_elements)
|
||||
.chain()
|
||||
.after(SceneSpawnerSystem),
|
||||
);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
@@ -35,6 +44,23 @@ pub struct Selected;
|
||||
#[derive(Resource, Deref, DerefMut, Debug)]
|
||||
pub struct Selection(Entity);
|
||||
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
|
||||
q_primitives: Query<&Handle<Mesh>>,
|
||||
) {
|
||||
for (ent, children) in q_scene_elements.iter() {
|
||||
cmd.entity(ent).insert(Selectable);
|
||||
|
||||
for &ent_child in children
|
||||
.iter()
|
||||
.filter(|child| q_primitives.contains(**child))
|
||||
{
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
mut cmd: Commands,
|
||||
// FIXME: These events aren't aware of our input actions, so we receive a
|
||||
|
||||
@@ -88,7 +88,7 @@ mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{self, WasmMessageBuffer};
|
||||
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::{TransformMode, TransformSpace, TransformType};
|
||||
|
||||
@@ -111,22 +111,22 @@ mod wasm {
|
||||
|
||||
#[wasm_bindgen(js_name = setTransformType)]
|
||||
pub fn set_transform_type(flags: u8) {
|
||||
wasm::write_message!(TRANSFORM_TYPE_BUF, flags.into());
|
||||
TRANSFORM_TYPE_BUF.write_message(flags.into());
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setTransformSpace)]
|
||||
pub fn set_transform_space(space: TransformSpace) {
|
||||
wasm::write_message!(TRANSFORM_SPACE_BUF, space);
|
||||
TRANSFORM_SPACE_BUF.write_message(space);
|
||||
}
|
||||
|
||||
pub(super) fn update_transform_mode(mut xform_mode: ResMut<TransformMode>) {
|
||||
wasm::read_buffer!(TRANSFORM_TYPE_BUF, |ttype| {
|
||||
if let Some(ttype) = TRANSFORM_TYPE_BUF.take_message() {
|
||||
info!("Setting transform type: {ttype:?}");
|
||||
xform_mode.ttype = ttype;
|
||||
});
|
||||
wasm::read_buffer!(TRANSFORM_SPACE_BUF, |space| {
|
||||
}
|
||||
if let Some(space) = TRANSFORM_SPACE_BUF.take_message() {
|
||||
info!("Setting transform space: {space:?}");
|
||||
xform_mode.space = space;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+89
-12
@@ -2,27 +2,36 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_picking::DefaultPickingPlugins;
|
||||
use camera_controller::CameraControllerPlugin;
|
||||
use debug::DebugPlugin;
|
||||
use input::StudioInputPlugin;
|
||||
use interaction::InteractionPlugin;
|
||||
use math::InterpolationPlugin;
|
||||
use scene::ScenePlugin;
|
||||
use skybox::SkyboxPlugin;
|
||||
use ui::UiLayerPlugin;
|
||||
use viewport::ViewportSize;
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod camera_controller;
|
||||
mod debug;
|
||||
mod design_system;
|
||||
mod input;
|
||||
mod inspector;
|
||||
mod interaction;
|
||||
mod math;
|
||||
mod scene;
|
||||
mod skybox;
|
||||
mod ui;
|
||||
mod viewport;
|
||||
mod world_grid;
|
||||
|
||||
use crate::{
|
||||
camera_controller::CameraControllerPlugin,
|
||||
debug::DebugPlugin,
|
||||
input::StudioInputPlugin,
|
||||
inspector::InspectorPlugin,
|
||||
interaction::InteractionPlugin,
|
||||
math::InterpolationPlugin,
|
||||
scene::{LoadSceneEvent, ScenePlugin},
|
||||
skybox::{SkyboxId, SkyboxPlugin},
|
||||
ui::UiLayerPlugin,
|
||||
viewport::ViewportSize,
|
||||
world_grid::WorldGridPlugin,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm;
|
||||
@@ -30,7 +39,7 @@ mod wasm;
|
||||
#[derive(Component)]
|
||||
pub(crate) struct MainCamera;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
pub fn init(viewport_size: ViewportSize) {
|
||||
#[cfg(feature = "wasm")]
|
||||
console_error_panic_hook::set_once();
|
||||
@@ -62,12 +71,80 @@ pub fn init(viewport_size: ViewportSize) {
|
||||
DefaultPickingPlugins,
|
||||
DebugPlugin,
|
||||
InteractionPlugin,
|
||||
InspectorPlugin,
|
||||
StudioInputPlugin,
|
||||
InterpolationPlugin,
|
||||
CameraControllerPlugin,
|
||||
ScenePlugin,
|
||||
SkyboxPlugin,
|
||||
UiLayerPlugin,
|
||||
))
|
||||
.run();
|
||||
WorldGridPlugin,
|
||||
));
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
wasm::initialize();
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = startViewer))]
|
||||
/// Start the application as a read-only viewer for a single library object.
|
||||
///
|
||||
/// @param object_id ID of the library object to load.
|
||||
/// @param skybox_id ID of the Skybox to use
|
||||
/// @param canvas_selector A CSS selector for the canvas that will host the Bevy
|
||||
/// application. This should be unique (i.e. `querySelectorAll` should not
|
||||
/// return more than one element), so using an ID selector is a good idea.
|
||||
/// @param canvas_alt `alt` text for the HTML canvas element.
|
||||
/// @param viewport_size The size of the canvas in CSS `px` units.
|
||||
pub fn start_viewer(
|
||||
object_id: &str,
|
||||
skybox_id: &str,
|
||||
canvas_selector: &str,
|
||||
canvas_alt: &str,
|
||||
viewport_size: ViewportSize,
|
||||
) {
|
||||
#[cfg(feature = "wasm")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let object_id = object_id.to_string();
|
||||
let skybox_id = skybox_id.to_string();
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_plugins((
|
||||
#[cfg(feature = "wasm")]
|
||||
viewport::wasm::ViewportPlugin,
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
resolution: viewport_size.into(),
|
||||
title: canvas_alt.into(),
|
||||
canvas: Some(canvas_selector.into()),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
DefaultPickingPlugins,
|
||||
DebugPlugin,
|
||||
InteractionPlugin,
|
||||
InspectorPlugin,
|
||||
StudioInputPlugin,
|
||||
InterpolationPlugin,
|
||||
CameraControllerPlugin,
|
||||
ScenePlugin,
|
||||
SkyboxPlugin,
|
||||
UiLayerPlugin,
|
||||
WorldGridPlugin,
|
||||
))
|
||||
.add_systems(
|
||||
Startup,
|
||||
move |mut cmd: Commands, mut ew: EventWriter<LoadSceneEvent>| {
|
||||
cmd.insert_resource(SkyboxId(skybox_id.clone()));
|
||||
ew.send(LoadSceneEvent(object_id.clone()));
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
wasm::initialize();
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
+50
-1
@@ -1,7 +1,7 @@
|
||||
use bevy::{
|
||||
math::vec3,
|
||||
prelude::*,
|
||||
render::{mesh::Indices, render_resource::PrimitiveTopology},
|
||||
render::{mesh::Indices, primitives::Aabb, render_resource::PrimitiveTopology},
|
||||
};
|
||||
|
||||
pub struct Cone {
|
||||
@@ -111,3 +111,52 @@ impl From<Plane2Sided> for Mesh {
|
||||
mesh
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Extrema {
|
||||
type Output: Sized;
|
||||
|
||||
fn extrema(&self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl Extrema for Aabb {
|
||||
type Output = [Vec3; 8];
|
||||
|
||||
fn extrema(&self) -> Self::Output {
|
||||
let min = Vec3::from(self.min());
|
||||
let max = Vec3::from(self.max());
|
||||
|
||||
[
|
||||
min,
|
||||
vec3(min.x, min.y, max.z),
|
||||
vec3(min.x, max.y, min.z),
|
||||
vec3(max.x, min.y, min.z),
|
||||
vec3(min.x, max.y, max.z),
|
||||
vec3(max.x, min.y, max.z),
|
||||
vec3(max.x, max.y, min.z),
|
||||
max,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Combinable {
|
||||
type IterItem<'a>;
|
||||
type Output;
|
||||
|
||||
fn combine<'a>(elements: impl Iterator<Item = Self::IterItem<'a>>) -> Self::Output;
|
||||
}
|
||||
|
||||
impl Combinable for Aabb {
|
||||
type IterItem<'a> = (&'a Self, &'a GlobalTransform);
|
||||
type Output = Option<Self>;
|
||||
|
||||
fn combine<'a>(elements: impl Iterator<Item = Self::IterItem<'a>>) -> Self::Output {
|
||||
let points = elements.flat_map(|(aabb, world_xform)| {
|
||||
let min = world_xform.transform_point(aabb.min().into());
|
||||
let max = world_xform.transform_point(aabb.max().into());
|
||||
|
||||
[min, max]
|
||||
});
|
||||
|
||||
Aabb::enclosing(points)
|
||||
}
|
||||
}
|
||||
|
||||
+352
-493
@@ -4,80 +4,114 @@ use bevy::{
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
gltf::{self, Gltf},
|
||||
math::vec3,
|
||||
pbr::{NotShadowCaster, ShadowFilteringMethod},
|
||||
gltf::Gltf,
|
||||
math::{vec2, vec3},
|
||||
pbr::ShadowFilteringMethod,
|
||||
prelude::*,
|
||||
reflect::TypePath,
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
utils::HashSet,
|
||||
render::primitives::Aabb,
|
||||
scene::InstanceId,
|
||||
};
|
||||
use bevy_mod_picking::PickableBundle;
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
|
||||
interaction::Selectable,
|
||||
skybox::SkyboxId,
|
||||
camera_controller::{CameraController, CameraControllerBundle},
|
||||
debug::DebugPoint,
|
||||
math::{Combinable, Extrema},
|
||||
MainCamera,
|
||||
};
|
||||
|
||||
pub struct ScenePlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct SceneSpawnerSystem;
|
||||
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||
.insert_resource(Msaa::Off)
|
||||
.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
|
||||
.add_state::<SceneState>()
|
||||
.add_event::<LoadSceneEvent>()
|
||||
.add_systems(Startup, initialize)
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
spawn_new_gltf_scene.run_if(
|
||||
resource_exists::<NewGltf>()
|
||||
.and_then(new_gltf_loaded)
|
||||
.and_then(run_once()),
|
||||
),
|
||||
Update,
|
||||
(
|
||||
init_scene_loading,
|
||||
tick_scene_loading.run_if(in_state(SceneState::Loading)),
|
||||
tick_scene_spawning.run_if(in_state(SceneState::Spawning)),
|
||||
)
|
||||
.in_set(SceneSpawnerSystem),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
mark_scene_objects.run_if(
|
||||
resource_exists::<NewGltf>()
|
||||
.and_then(new_gltf_loaded)
|
||||
.and_then(gltf_scene_spawned),
|
||||
),
|
||||
OnEnter(SceneState::Loading),
|
||||
despawn_previous_scene.in_set(SceneSpawnerSystem),
|
||||
)
|
||||
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Spawning),
|
||||
spawn_next_scene.in_set(SceneSpawnerSystem),
|
||||
)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(
|
||||
tag_scene_elements,
|
||||
spawn_main_scene_light,
|
||||
frame_scene_in_view,
|
||||
)
|
||||
.in_set(SceneSpawnerSystem),
|
||||
);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
wasm::notify_entity_spawn_changes.map(bevy::utils::error),
|
||||
Update,
|
||||
(
|
||||
wasm::dispatch_scene_request,
|
||||
wasm::notify_state_changes
|
||||
.map(bevy::utils::error)
|
||||
.run_if(resource_changed::<State<SceneState>>()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct WorldGridPlane;
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(States, Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SceneState {
|
||||
#[default]
|
||||
None,
|
||||
Loading,
|
||||
Spawning,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
struct NewGltf(pub Handle<Gltf>);
|
||||
struct PendingScene(pub Handle<Gltf>);
|
||||
|
||||
#[derive(Resource, Deref)]
|
||||
pub struct ActiveScene {
|
||||
#[deref]
|
||||
pub instance: InstanceId,
|
||||
pub source: Handle<Gltf>,
|
||||
}
|
||||
|
||||
#[derive(Event, Deref)]
|
||||
pub struct LoadSceneEvent(pub String);
|
||||
|
||||
// TODOL This could be converted into an enum that indicates what "kind" of
|
||||
// element this is (e.g. StaticMesh, SkinnedMesh, Bone, Light, Camera).
|
||||
// We don't have an immediate need for that just yet, but likely will
|
||||
// soon.
|
||||
/// A tag that identifies its entity as a user-facing scene element like a prop,
|
||||
/// a light, or a camera.
|
||||
#[derive(Component)]
|
||||
pub struct SceneElement;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Inspectable;
|
||||
|
||||
fn initialize(
|
||||
mut cmd: Commands,
|
||||
assets: Res<AssetServer>,
|
||||
mut ra_meshes: ResMut<Assets<Mesh>>,
|
||||
mut ra_mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||
) {
|
||||
// Skybox
|
||||
cmd.insert_resource(SkyboxId("test_scene".into()));
|
||||
|
||||
// glTF scene
|
||||
cmd.insert_resource(NewGltf(assets.load("gltf/sample-room.gltf")));
|
||||
pub struct SceneRoot;
|
||||
|
||||
fn initialize(mut cmd: Commands) {
|
||||
// Camera, graphics settings
|
||||
let focus = vec3(0.35011, 0.5, -0.99576);
|
||||
let focus = Vec3::ZERO;
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
@@ -104,495 +138,320 @@ fn initialize(
|
||||
},
|
||||
Fxaa::default(),
|
||||
));
|
||||
|
||||
// Main light
|
||||
// TODO: get Blender to export enough metadata to automate light placements
|
||||
cmd.spawn((
|
||||
Name::new("Ceiling Light"),
|
||||
Selectable,
|
||||
Inspectable,
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::hex("FFE5B4").unwrap(),
|
||||
intensity: 200.,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Monitor glow
|
||||
cmd.spawn((
|
||||
Name::new("Monitor Glow"),
|
||||
Selectable,
|
||||
Inspectable,
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::WHITE,
|
||||
intensity: 6.,
|
||||
radius: 0.25,
|
||||
shadows_enabled: false,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Plane
|
||||
let plane = ra_meshes.add(Mesh::from(shape::Plane {
|
||||
size: 200.0,
|
||||
subdivisions: 0,
|
||||
}));
|
||||
let proc_grid = ra_mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("World Grid"),
|
||||
SpatialBundle::from_transform(Transform::IDENTITY),
|
||||
WorldGridPlane,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane.clone(),
|
||||
material: proc_grid.clone(),
|
||||
transform: Transform::IDENTITY,
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane,
|
||||
material: proc_grid,
|
||||
transform: Transform::IDENTITY.looking_to(Vec3::Z, Vec3::NEG_Y),
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LoadState(Handle<Gltf>, bool);
|
||||
fn init_scene_loading(
|
||||
mut cmd: Commands,
|
||||
assets: Res<AssetServer>,
|
||||
mut er: EventReader<LoadSceneEvent>,
|
||||
mut st_next: ResMut<NextState<SceneState>>,
|
||||
) {
|
||||
if let Some(id) = er.read().last() {
|
||||
let name = &**id;
|
||||
cmd.insert_resource(PendingScene(assets.load(format!("gltf/{name}.gltf"))));
|
||||
st_next.set(SceneState::Loading);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_gltf_loaded(
|
||||
r_gltf: Res<NewGltf>,
|
||||
fn despawn_previous_scene(mut cmd: Commands, q_scene_root: Query<Entity, With<SceneRoot>>) {
|
||||
if let Ok(scene_root) = q_scene_root.get_single() {
|
||||
cmd.entity(scene_root).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_scene_loading(
|
||||
r_pending_scene: Res<PendingScene>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
mut er_scene_events: EventReader<AssetEvent<Scene>>,
|
||||
mut l_loaded: Local<LoadState>,
|
||||
) -> bool {
|
||||
if l_loaded.0.path() != r_gltf.path() {
|
||||
*l_loaded = LoadState(r_gltf.clone(), false);
|
||||
} else if l_loaded.1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
l_loaded.1 = er_scene_events.read().any(|event| match event {
|
||||
AssetEvent::LoadedWithDependencies { id } => {
|
||||
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
|
||||
let Some(default_scene) = &gltf.default_scene else {
|
||||
return false;
|
||||
};
|
||||
|
||||
*id == default_scene.id()
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
l_loaded.1
|
||||
}
|
||||
|
||||
fn spawn_new_gltf_scene(mut cmd: Commands, ra_gltfs: Res<Assets<Gltf>>, r_gltf: Res<NewGltf>) {
|
||||
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
|
||||
let Some(scene) = &gltf.default_scene else {
|
||||
return;
|
||||
};
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("Scene"),
|
||||
Inspectable,
|
||||
SceneBundle {
|
||||
scene: scene.clone(),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn gltf_scene_spawned(
|
||||
r_gltf: Res<NewGltf>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
q_scenes: Query<&Handle<Scene>, (With<Name>, With<Children>)>,
|
||||
) -> bool {
|
||||
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
|
||||
|
||||
let Some(gltf_scene_path) = gltf.default_scene.as_ref().and_then(|handle| handle.path()) else {
|
||||
warn!("No default scene found for glTF: {:#?}", gltf);
|
||||
return false;
|
||||
};
|
||||
|
||||
q_scenes.iter().any(|handle| {
|
||||
let Some(path) = handle.path() else {
|
||||
return false;
|
||||
};
|
||||
path == gltf_scene_path
|
||||
})
|
||||
}
|
||||
|
||||
fn mark_scene_objects(
|
||||
mut cmd: Commands,
|
||||
r_gltf: Res<NewGltf>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
ra_gltf_nodes: Res<Assets<gltf::GltfNode>>,
|
||||
ra_gltf_meshes: Res<Assets<gltf::GltfMesh>>,
|
||||
q_parents: Query<(Entity, &Name, &Children), Without<Selectable>>,
|
||||
q_primitives: Query<&Handle<Mesh>>,
|
||||
mut st_next: ResMut<NextState<SceneState>>,
|
||||
) {
|
||||
info!("mark_scene_objects");
|
||||
|
||||
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
|
||||
|
||||
for (ent_parent, name, children) in q_parents.iter() {
|
||||
if gltf.named_nodes.contains_key(&**name) {
|
||||
cmd.entity(ent_parent).insert(Inspectable);
|
||||
}
|
||||
|
||||
// See if this name matches any of the meshes or nodes in our glTF scene
|
||||
let Some(gltf_mesh) = gltf
|
||||
.named_meshes
|
||||
.get(&**name)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
gltf.named_nodes.get(&**name).and_then(|node_handle| {
|
||||
ra_gltf_nodes
|
||||
.get(node_handle)
|
||||
.and_then(|node| node.mesh.clone())
|
||||
})
|
||||
})
|
||||
.and_then(|handle| ra_gltf_meshes.get(&handle))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// FIXME: These hashsets don't actually contain anything. Ever. -_-
|
||||
// This has "worked" this whole time because {} == {}
|
||||
|
||||
// Collect all of the mesh handles in the glTF mesh
|
||||
let prims_from_gltf = gltf_mesh
|
||||
.primitives
|
||||
.iter()
|
||||
.filter_map(|prim| prim.mesh.path())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// Collect all of the mesh handles in this entity's children
|
||||
let prims_from_ecs = children
|
||||
.iter()
|
||||
.filter_map(|ent_child| {
|
||||
q_primitives
|
||||
.get(*ent_child)
|
||||
.ok()
|
||||
.and_then(|handle| handle.path())
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// If the sets match, this is the entity that represents the glTF mesh
|
||||
if prims_from_gltf == prims_from_ecs {
|
||||
cmd.entity(ent_parent).insert(Selectable);
|
||||
|
||||
for &ent_child in children.iter() {
|
||||
if q_primitives.contains(ent_child) {
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
}
|
||||
for event in er_scene_events.read() {
|
||||
if let AssetEvent::LoadedWithDependencies { id } = event {
|
||||
let gltf = ra_gltfs.get(&**r_pending_scene).unwrap();
|
||||
if gltf.default_scene.is_some() && gltf.default_scene.as_ref().unwrap().id() == *id {
|
||||
st_next.set(SceneState::Spawning);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.remove_resource::<NewGltf>();
|
||||
}
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&CameraController, Changed<CameraController>>,
|
||||
fn spawn_next_scene(
|
||||
mut cmd: Commands,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
r_pending_scene: Res<PendingScene>,
|
||||
mut r_scene_spawner: ResMut<SceneSpawner>,
|
||||
) {
|
||||
if let Ok(&CameraController { focus, .. }) = camera.get_single() {
|
||||
let (_, mut xform) = plane.single_mut();
|
||||
let gltf = ra_gltfs.get(&**r_pending_scene).unwrap();
|
||||
let scene = gltf
|
||||
.default_scene
|
||||
.as_ref()
|
||||
.expect("Expected glTF to include a default scene");
|
||||
|
||||
xform.translation.x = focus.x;
|
||||
xform.translation.z = focus.z;
|
||||
let parent = cmd
|
||||
.spawn((
|
||||
Name::new("Scene"),
|
||||
SceneRoot,
|
||||
SceneElement,
|
||||
SpatialBundle::default(),
|
||||
))
|
||||
.id();
|
||||
|
||||
let instance = r_scene_spawner.spawn_as_child(scene.clone(), parent);
|
||||
|
||||
cmd.remove_resource::<PendingScene>();
|
||||
cmd.insert_resource(ActiveScene {
|
||||
instance,
|
||||
source: r_pending_scene.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn tick_scene_spawning(
|
||||
r_scene_spawner: Res<SceneSpawner>,
|
||||
r_active_scene: Res<ActiveScene>,
|
||||
mut st_next: ResMut<NextState<SceneState>>,
|
||||
) {
|
||||
// TODO: The instance is considered "ready" even if shaders still need to be
|
||||
// compiled, so this event isn't really sufficient for controlling a
|
||||
// loading screen.
|
||||
if r_scene_spawner.instance_is_ready(**r_active_scene) {
|
||||
st_next.set(SceneState::Active);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Asset, AsBindGroup, TypePath, Clone)]
|
||||
struct ProceduralGridMaterial {}
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
r_scene_spawner: Res<SceneSpawner>,
|
||||
r_active_scene: Res<ActiveScene>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
// A "glTF mesh" is actually a container for one or more "glTF primitives",
|
||||
// which is basically a bundle of vertices paired with the material used to
|
||||
// shade them. It's those "sub-meshes" that carry the actual mesh handles,
|
||||
// but we don't necessarily want to expose those to users a la carte,
|
||||
// because they often consist of stuff like a single eyeball, or just the
|
||||
// leafy parts of a plant. Here we only want to tag the elements that a user
|
||||
// might want to individually address when arranging or animating a scene.
|
||||
q_named_non_primitives: Query<&Name, Without<Handle<Mesh>>>,
|
||||
) {
|
||||
let gltf = ra_gltfs.get(&r_active_scene.source).unwrap();
|
||||
|
||||
impl Material for ProceduralGridMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"shaders/procedural_grid.wgsl".into()
|
||||
}
|
||||
for ent in r_scene_spawner.iter_instance_entities(**r_active_scene) {
|
||||
let Ok(name) = q_named_non_primitives.get(ent) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
fn alpha_mode(&self) -> AlphaMode {
|
||||
AlphaMode::Blend
|
||||
if gltf.named_nodes.contains_key(&**name) {
|
||||
cmd.entity(ent).insert(SceneElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_main_scene_light(
|
||||
mut cmd: Commands,
|
||||
r_scene_spawner: Res<SceneSpawner>,
|
||||
r_active_scene: Res<ActiveScene>,
|
||||
q_aabbs: Query<(&Aabb, &GlobalTransform)>,
|
||||
q_scene_root: Query<Entity, With<SceneRoot>>,
|
||||
) {
|
||||
let Ok(scene_root) = q_scene_root.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(aabb) = Aabb::combine(
|
||||
r_scene_spawner
|
||||
.iter_instance_entities(**r_active_scene)
|
||||
.filter_map(|ent| q_aabbs.get(ent).ok()),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let light_offset_base = vec3(0.5, 1., 1.).normalize();
|
||||
let size = f32::max(
|
||||
f32::max(aabb.half_extents.x, aabb.half_extents.y),
|
||||
aabb.half_extents.z,
|
||||
) * 2.;
|
||||
let light_pos = Vec3::from(aabb.center) + light_offset_base * size * 2.;
|
||||
|
||||
cmd.spawn(DebugPoint {
|
||||
point: light_pos,
|
||||
color: Color::WHITE,
|
||||
});
|
||||
cmd.entity(scene_root).with_children(|cmd| {
|
||||
cmd.spawn((
|
||||
Name::new("Light"),
|
||||
SceneElement,
|
||||
SpotLightBundle {
|
||||
spot_light: SpotLight {
|
||||
color: Color::WHITE,
|
||||
// TODO: This doesn't scale super well to very large objects
|
||||
intensity: 200. * size,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
shadow_normal_bias: 0.2 * size,
|
||||
outer_angle: f32::to_radians(150.),
|
||||
inner_angle: f32::to_radians(45.),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_translation(light_pos)
|
||||
.looking_at(aabb.center.into(), Vec3::Y),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn frame_scene_in_view(
|
||||
mut cmd: Commands,
|
||||
r_scene_spawner: Res<SceneSpawner>,
|
||||
r_active_scene: Res<ActiveScene>,
|
||||
q_aabbs: Query<(&Aabb, &GlobalTransform)>,
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>,
|
||||
) {
|
||||
let Ok((mut cam, mut cam_xform, cam_proj)) = q_camera.get_single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(aabb) = Aabb::combine(
|
||||
r_scene_spawner
|
||||
.iter_instance_entities(**r_active_scene)
|
||||
.filter_map(|ent| q_aabbs.get(ent).ok()),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let world2cam = cam_xform.compute_matrix().inverse();
|
||||
let plane_origin = Vec3::from(aabb.center);
|
||||
let plane_normal = cam_xform.forward();
|
||||
|
||||
let Some((_depth, bounding_rect)) = aabb
|
||||
.extrema()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|mut p| {
|
||||
cmd.spawn(DebugPoint {
|
||||
point: p,
|
||||
color: Color::CYAN,
|
||||
});
|
||||
|
||||
// Inflate the combined AABB to give the model some breathing room
|
||||
p *= 1.5;
|
||||
|
||||
// Project each corner onto the camera plane
|
||||
let v = p - plane_origin;
|
||||
let proj = p - v.project_onto_normalized(plane_normal);
|
||||
|
||||
cmd.spawn(DebugPoint {
|
||||
point: proj,
|
||||
color: Color::FUCHSIA,
|
||||
});
|
||||
|
||||
// Transform into the camera's local space
|
||||
world2cam.transform_point(proj)
|
||||
})
|
||||
// Fold the camera-space plane-projected points into a bounding rect and view depth
|
||||
.fold(None, |accum: Option<(f32, Rect)>, p: Vec3| match accum {
|
||||
Some((depth, rect)) => Some((depth, rect.union_point(vec2(p.x, p.y)))),
|
||||
None => {
|
||||
let depth = p.z.abs();
|
||||
let p = vec2(p.x, p.y);
|
||||
Some((depth, Rect::from_corners(p, p)))
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pproj = match cam_proj {
|
||||
Projection::Perspective(p) => p,
|
||||
_ => panic!("Expected a perspective projection"),
|
||||
};
|
||||
|
||||
// Formula for the target camera distance
|
||||
// (c/o https://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene):
|
||||
//
|
||||
// d = (s/2) / tan(a/2)
|
||||
// where:
|
||||
// d = distance
|
||||
// s = size of the largest camera-aligned axis in world coords
|
||||
// a = field-of-view angle for the same axis
|
||||
let Vec2 {
|
||||
x: width,
|
||||
y: height,
|
||||
} = bounding_rect.size();
|
||||
|
||||
let s = f32::max(width, height);
|
||||
let a = if s == height {
|
||||
pproj.fov
|
||||
} else {
|
||||
f32::atan(pproj.aspect_ratio * f32::tan(pproj.fov / 2.))
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// According to "I eat babies" at the Ars Technica forums
|
||||
// (https://arstechnica.com/civis/threads/if-you-have-aspect-ratio-and-vertical-fov-can-you-calc-horizontal-fov.37447/):
|
||||
//
|
||||
// tan(fov_v / 2) = y / z
|
||||
// tan(fov_h / 2) = x / z
|
||||
//
|
||||
// Theoretically, this would let us skip some costly tan/atan
|
||||
// computations, but I must be missing something, because just
|
||||
// substituting (s / depth) for the divisor below would make the
|
||||
// whole equation independent of the FOV, which is obviously
|
||||
// wrong.
|
||||
//
|
||||
// Unfortunately I'm embarrassingly bad at algebra, but there
|
||||
// must be some room for simplification here.
|
||||
let radius = (s / 2.) / f32::tan(a / 2.);
|
||||
|
||||
cam.focus = aabb.center.into();
|
||||
cam.radius = radius;
|
||||
cam_xform.translation = cam.focus + cam_xform.back() * radius;
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::{prelude::*, utils::HashSet, window::PrimaryWindow};
|
||||
use serde::Serialize;
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{self, AsJson};
|
||||
use super::{LoadSceneEvent, SceneState};
|
||||
use crate::wasm::{self, StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::Inspectable;
|
||||
static OBJECT_LOAD_REQUEST: WasmMessageBuffer<String> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = loadObject)]
|
||||
pub fn load_object(id: &str) {
|
||||
OBJECT_LOAD_REQUEST.write_message(id.into());
|
||||
}
|
||||
|
||||
// FIXME: Is it an object or a scene? Inconsistent terminology is
|
||||
// unnecessarily confusing
|
||||
pub(super) fn dispatch_scene_request(mut ew_load_scene: EventWriter<LoadSceneEvent>) {
|
||||
if let Some(id) = OBJECT_LOAD_REQUEST.take_message() {
|
||||
ew_load_scene.send(LoadSceneEvent(id));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TYPE_DECLARATIONS: &str = r#"
|
||||
export interface SceneObject {
|
||||
/** The entity ID. */
|
||||
id: string;
|
||||
/** A user-friendly name for displaying in the UI */
|
||||
name: string;
|
||||
/** The IDs of this object's children in the hierarchy */
|
||||
children: string[];
|
||||
/** The ID of this object's parent in the hierarchy, if it has one */
|
||||
parent: string | null;
|
||||
/** The local-space transform */
|
||||
transform: Transform;
|
||||
/** The world-space transform */
|
||||
globalTransform: GlobalTransform;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that a new entity has spawned.
|
||||
*/
|
||||
export interface EntitySpawnEvent extends CustomEvent {
|
||||
type: "entity-spawn";
|
||||
detail: SceneObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that an entity has been de-spawned.
|
||||
*/
|
||||
export interface EntityDespawnEvent extends CustomEvent {
|
||||
type: "entity-despawn";
|
||||
/** The entity ID */
|
||||
detail: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event indicating that multiple entities have spawned.
|
||||
*/
|
||||
export interface EntityMultiSpawnEvent extends CustomEvent {
|
||||
type: "entity-multi-spawn";
|
||||
detail: SceneObject[];
|
||||
const SCENE_STATE_EVENT: &str = r#"
|
||||
export interface SceneStateEvent extends CustomEvent {
|
||||
type: "scene-state";
|
||||
detail: SceneState;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface HTMLElementEventMap {
|
||||
"entity-spawn": EntitySpawnEvent;
|
||||
"entity-despawn": EntityDespawnEvent;
|
||||
"entity-multi-spawn": EntityMultiSpawnEvent;
|
||||
"scene-state": SceneStateEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct SceneObject {
|
||||
id: String,
|
||||
name: String,
|
||||
children: Vec<String>,
|
||||
parent: Option<String>,
|
||||
transform: serde_json::Value,
|
||||
#[serde(rename(serialize = "globalTransform"))]
|
||||
global_transform: serde_json::Value,
|
||||
}
|
||||
|
||||
struct SceneObjectBuilder<'a> {
|
||||
id: Entity,
|
||||
name: Option<String>,
|
||||
children: Option<Vec<Entity>>,
|
||||
parent: Option<Entity>,
|
||||
transform: Option<&'a Transform>,
|
||||
global_transform: Option<&'a GlobalTransform>,
|
||||
}
|
||||
|
||||
impl<'a> SceneObjectBuilder<'a> {
|
||||
fn new(id: Entity) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name: None,
|
||||
children: None,
|
||||
parent: None,
|
||||
transform: None,
|
||||
global_transform: None,
|
||||
}
|
||||
}
|
||||
fn finish(self) -> SceneObject {
|
||||
SceneObject {
|
||||
id: format!("{:?}", self.id),
|
||||
name: self.name.unwrap(),
|
||||
children: self
|
||||
.children
|
||||
.map(|children| children.iter().map(|ent| format!("{ent:?}")).collect())
|
||||
.unwrap_or_default(),
|
||||
parent: self.parent.map(|ent| format!("{ent:?}")),
|
||||
transform: self.transform.unwrap().as_json(),
|
||||
global_transform: self.global_transform.unwrap().as_json(),
|
||||
}
|
||||
}
|
||||
fn with_name(mut self, name: String) -> Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
fn with_children(mut self, children: Vec<Entity>) -> Self {
|
||||
self.children = Some(children);
|
||||
self
|
||||
}
|
||||
fn with_parent(mut self, parent: Entity) -> Self {
|
||||
self.parent = Some(parent);
|
||||
self
|
||||
}
|
||||
fn with_transform(mut self, transform: &'a Transform) -> Self {
|
||||
self.transform = Some(transform);
|
||||
self
|
||||
}
|
||||
fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self {
|
||||
self.global_transform = Some(global_transform);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SceneObject {
|
||||
fn build(id: Entity) -> SceneObjectBuilder<'a> {
|
||||
SceneObjectBuilder::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn notify_entity_spawn_changes(
|
||||
mut l_notified: Local<HashSet<Entity>>,
|
||||
// Note: We use locals for these data structures purely to reuse
|
||||
// existing allocations across multiple invocations of this system
|
||||
mut l_new_objects: Local<Vec<SceneObject>>,
|
||||
mut l_despawned_entities: Local<Vec<Entity>>,
|
||||
mut l_missing_parents: Local<VecDeque<Entity>>,
|
||||
pub(super) fn notify_state_changes(
|
||||
r_state: Res<State<SceneState>>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With<Inspectable>>,
|
||||
q_others: Query<(Entity, &Transform, &GlobalTransform), Without<Inspectable>>,
|
||||
q_names: Query<&Name>,
|
||||
q_parents: Query<&Children>,
|
||||
q_children: Query<&Parent>,
|
||||
) -> Result<(), String> {
|
||||
l_missing_parents.clear();
|
||||
|
||||
l_new_objects.clear();
|
||||
l_new_objects.extend(
|
||||
q_inspectables
|
||||
.iter()
|
||||
.filter_map(|(ent, xform, world_xform)| {
|
||||
build_scene_object(
|
||||
&mut l_notified,
|
||||
&mut l_missing_parents,
|
||||
ent,
|
||||
xform,
|
||||
world_xform,
|
||||
&q_inspectables,
|
||||
&q_names,
|
||||
&q_parents,
|
||||
&q_children,
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
while let Some(parent) = l_missing_parents.pop_front() {
|
||||
let Ok((ent, xform, world_xform)) = q_others.get(parent) else {
|
||||
error!("Failed to find missing parent: {parent:?}");
|
||||
l_notified.insert(parent);
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(object) = build_scene_object(
|
||||
&mut l_notified,
|
||||
&mut l_missing_parents,
|
||||
ent,
|
||||
xform,
|
||||
world_xform,
|
||||
&q_inspectables,
|
||||
&q_names,
|
||||
&q_parents,
|
||||
&q_children,
|
||||
) {
|
||||
l_new_objects.push(object);
|
||||
}
|
||||
if let Ok(window) = q_window.get_single() {
|
||||
wasm::dispatch_to_js(window, "scene-state", &(**r_state as u32))?;
|
||||
}
|
||||
|
||||
l_despawned_entities.clear();
|
||||
l_despawned_entities.extend(
|
||||
l_notified
|
||||
.iter()
|
||||
.filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
let win = q_window.single();
|
||||
|
||||
for ent in l_despawned_entities.iter() {
|
||||
// TODO: Make this take an array like the multi-spawn event
|
||||
wasm::dispatch_to_js(win, "entity-despawn", ent)?;
|
||||
l_notified.remove(ent);
|
||||
}
|
||||
|
||||
if !l_new_objects.is_empty() {
|
||||
// TODO: Remove the single-spawn event
|
||||
wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_scene_object(
|
||||
notified: &mut HashSet<Entity>,
|
||||
missing_parents: &mut VecDeque<Entity>,
|
||||
ent: Entity,
|
||||
xform: &Transform,
|
||||
world_xform: &GlobalTransform,
|
||||
q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With<Inspectable>>,
|
||||
q_names: &Query<&Name>,
|
||||
q_parents: &Query<&Children>,
|
||||
q_children: &Query<&Parent>,
|
||||
) -> Option<SceneObject> {
|
||||
if notified.contains(&ent) {
|
||||
return None;
|
||||
}
|
||||
|
||||
notified.insert(ent);
|
||||
|
||||
let mut result = SceneObject::build(ent)
|
||||
.with_transform(xform)
|
||||
.with_global_transform(world_xform);
|
||||
|
||||
if let Ok(children) = q_parents.get(ent) {
|
||||
result = result.with_children(children.iter().cloned().collect());
|
||||
}
|
||||
if let Ok(parent) = q_children.get(ent) {
|
||||
let parent_ent = **parent;
|
||||
result = result.with_parent(parent_ent);
|
||||
|
||||
if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) {
|
||||
missing_parents.push_back(parent_ent);
|
||||
}
|
||||
}
|
||||
|
||||
result = result.with_name(match q_names.get(ent) {
|
||||
Ok(name) => name.into(),
|
||||
Err(_) => format!("{ent:?}"),
|
||||
});
|
||||
|
||||
Some(result.finish())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ impl Plugin for SkyboxPlugin {
|
||||
.run_if(skybox_assets_loaded),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(Update, wasm::set_skybox_from_frontend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,3 +115,26 @@ impl SkyboxPaths {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::SkyboxId;
|
||||
|
||||
static SKYBOX_REQUEST: WasmMessageBuffer<String> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = setSkybox)]
|
||||
pub fn set_skybox(id: &str) {
|
||||
SKYBOX_REQUEST.write_message(id.to_string());
|
||||
}
|
||||
|
||||
pub(super) fn set_skybox_from_frontend(mut cmd: Commands) {
|
||||
if let Some(id) = SKYBOX_REQUEST.take_message() {
|
||||
cmd.insert_resource(SkyboxId(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{self, WasmMessageBuffer};
|
||||
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::ViewportSize;
|
||||
|
||||
@@ -58,7 +58,7 @@ pub mod wasm {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn resize(viewport_size: ViewportSize) {
|
||||
wasm::write_message!(RESIZE_BUF, viewport_size);
|
||||
RESIZE_BUF.write_message(viewport_size);
|
||||
}
|
||||
|
||||
pub struct ViewportPlugin;
|
||||
@@ -85,8 +85,8 @@ pub mod wasm {
|
||||
}
|
||||
|
||||
fn dispatch_resize_events(mut events: EventWriter<ViewportResizeEvent>) {
|
||||
wasm::read_buffer!(RESIZE_BUF, |resize| {
|
||||
if let Some(resize) = RESIZE_BUF.take_message() {
|
||||
events.send(ViewportResizeEvent(resize));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,3 +132,9 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsJsValue for u32 {
|
||||
fn as_js(&self) -> Result<JsValue, String> {
|
||||
Ok(JsValue::from_f64(*self as f64))
|
||||
}
|
||||
}
|
||||
|
||||
+18
-77
@@ -1,90 +1,31 @@
|
||||
use std::{cell::RefCell, sync::OnceLock};
|
||||
|
||||
use bevy::window::Window;
|
||||
use parking_lot::ReentrantMutex;
|
||||
use web_sys::{CustomEvent, CustomEventInit, Element};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
mod conv;
|
||||
mod util;
|
||||
|
||||
pub use conv::*;
|
||||
pub(crate) use util::*;
|
||||
|
||||
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
|
||||
static INITIALIZED: OnceLock<ReentrantMutex<RefCell<bool>>> = OnceLock::new();
|
||||
|
||||
pub(crate) fn dispatch_to_js(
|
||||
window: &Window,
|
||||
event_type: &str,
|
||||
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
|
||||
value: &dyn AsJsValue,
|
||||
) -> Result<(), String> {
|
||||
let canvas = try_get_canvas(window)?;
|
||||
/// The Studio application must only be initialized once (via `init` or
|
||||
/// `startViewer`). Multiple calls to either of those functions will cause the
|
||||
/// application to panic. This function can be used to query whether the app has
|
||||
/// already been initialized.
|
||||
#[wasm_bindgen(js_name = isInitialized)]
|
||||
pub fn is_initialized() -> bool {
|
||||
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
|
||||
let guard = wrapper.lock();
|
||||
let result = *guard.borrow();
|
||||
|
||||
let detail = value.as_js()?;
|
||||
let event = CustomEvent::new_with_event_init_dict(
|
||||
event_type,
|
||||
CustomEventInit::new()
|
||||
.bubbles(true)
|
||||
.cancelable(true)
|
||||
.detail(&detail),
|
||||
)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
canvas
|
||||
.dispatch_event(&event)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
Ok(())
|
||||
result
|
||||
}
|
||||
|
||||
fn try_get_canvas(window: &Window) -> Result<Element, String> {
|
||||
let canvas_selector = window
|
||||
.canvas
|
||||
.as_ref()
|
||||
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
|
||||
|
||||
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
|
||||
let document = window
|
||||
.document()
|
||||
.ok_or("window.document() returned None".to_string())?;
|
||||
|
||||
match document.query_selector(canvas_selector) {
|
||||
Ok(Some(canvas)) => Ok(canvas),
|
||||
Ok(None) => Err(format!(
|
||||
"Failed to find element with selector '{canvas_selector}'"
|
||||
)),
|
||||
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
|
||||
}
|
||||
pub(crate) fn initialize() {
|
||||
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
|
||||
let guard = wrapper.lock();
|
||||
*guard.borrow_mut() = true;
|
||||
}
|
||||
|
||||
macro_rules! get_buffer {
|
||||
($buffer:ident) => {
|
||||
$buffer.get_or_init(|| {
|
||||
::parking_lot::ReentrantMutex::new(::std::cell::RefCell::new(
|
||||
::core::option::Option::None,
|
||||
))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! write_message {
|
||||
($buffer:ident, $expr:expr) => {{
|
||||
let buffer = crate::wasm::get_buffer!($buffer);
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
*lock.borrow_mut() = Some($expr);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! read_buffer {
|
||||
($buffer:ident, |$msg:ident| $stmt:stmt) => {{
|
||||
let buffer = crate::wasm::get_buffer!($buffer);
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
if let Some($msg) = lock.borrow_mut().take() {
|
||||
$stmt
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use get_buffer;
|
||||
pub(crate) use read_buffer;
|
||||
pub(crate) use write_message;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::{cell::RefCell, sync::OnceLock};
|
||||
|
||||
use bevy::window::Window;
|
||||
use parking_lot::ReentrantMutex;
|
||||
use web_sys::{CustomEvent, CustomEventInit, Element};
|
||||
|
||||
use super::AsJsValue;
|
||||
|
||||
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
|
||||
|
||||
pub(crate) trait StaticBuffer<T> {
|
||||
type Inner;
|
||||
|
||||
fn inner(&self) -> &Self::Inner;
|
||||
fn write_message(&self, value: T);
|
||||
fn take_message(&self) -> Option<T>;
|
||||
}
|
||||
|
||||
impl<T> StaticBuffer<T> for WasmMessageBuffer<T> {
|
||||
type Inner = ReentrantMutex<RefCell<Option<T>>>;
|
||||
|
||||
#[inline]
|
||||
fn inner(&self) -> &Self::Inner {
|
||||
self.get_or_init(|| ReentrantMutex::new(RefCell::new(None)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_message(&self, value: T) {
|
||||
let mutex = self.inner();
|
||||
if let Some(guard) = mutex.try_lock() {
|
||||
*guard.borrow_mut() = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn take_message(&self) -> Option<T> {
|
||||
let mutex = self.inner();
|
||||
mutex.try_lock().and_then(|guard| guard.borrow_mut().take())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_to_js(
|
||||
window: &Window,
|
||||
event_type: &str,
|
||||
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
|
||||
value: &dyn AsJsValue,
|
||||
) -> Result<(), String> {
|
||||
let canvas = try_get_canvas(window)?;
|
||||
|
||||
let detail = value.as_js()?;
|
||||
let event = CustomEvent::new_with_event_init_dict(
|
||||
event_type,
|
||||
CustomEventInit::new()
|
||||
.bubbles(true)
|
||||
.cancelable(true)
|
||||
.detail(&detail),
|
||||
)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
canvas
|
||||
.dispatch_event(&event)
|
||||
.map_err(|err| format!("{err:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_get_canvas(window: &Window) -> Result<Element, String> {
|
||||
let canvas_selector = window
|
||||
.canvas
|
||||
.as_ref()
|
||||
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
|
||||
|
||||
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
|
||||
let document = window
|
||||
.document()
|
||||
.ok_or("window.document() returned None".to_string())?;
|
||||
|
||||
match document.query_selector(canvas_selector) {
|
||||
Ok(Some(canvas)) => Ok(canvas),
|
||||
Ok(None) => Err(format!(
|
||||
"Failed to find element with selector '{canvas_selector}'"
|
||||
)),
|
||||
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use bevy::{
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
};
|
||||
|
||||
use crate::camera_controller::{CameraController, CameraMotionSystem};
|
||||
|
||||
pub struct WorldGridPlugin;
|
||||
|
||||
impl Plugin for WorldGridPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
|
||||
.add_systems(Startup, spawn_world_grid)
|
||||
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct WorldGridPlane;
|
||||
|
||||
#[derive(Asset, AsBindGroup, TypePath, Clone)]
|
||||
struct ProceduralGridMaterial {}
|
||||
|
||||
impl Material for ProceduralGridMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"shaders/procedural_grid.wgsl".into()
|
||||
}
|
||||
|
||||
fn alpha_mode(&self) -> AlphaMode {
|
||||
AlphaMode::Blend
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_world_grid(
|
||||
mut cmd: Commands,
|
||||
mut ra_meshes: ResMut<Assets<Mesh>>,
|
||||
mut ra_mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||
) {
|
||||
let plane = ra_meshes.add(Mesh::from(shape::Plane {
|
||||
size: 200.0,
|
||||
subdivisions: 0,
|
||||
}));
|
||||
let proc_grid = ra_mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("World Grid"),
|
||||
SpatialBundle::from_transform(Transform::IDENTITY),
|
||||
WorldGridPlane,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane.clone(),
|
||||
material: proc_grid.clone(),
|
||||
transform: Transform::IDENTITY,
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane,
|
||||
material: proc_grid,
|
||||
transform: Transform::IDENTITY.looking_to(Vec3::Z, Vec3::NEG_Y),
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&CameraController, Changed<CameraController>>,
|
||||
) {
|
||||
if let Ok(&CameraController { focus, .. }) = camera.get_single() {
|
||||
let (_, mut xform) = plane.single_mut();
|
||||
|
||||
xform.translation.x = focus.x;
|
||||
xform.translation.z = focus.z;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user