chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:24:51 +08:00
commit e74a3762b8
963 changed files with 299400 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! All possible errors that can happen in the engine.
use crate::{graphics::error::FrameworkError, scene::sound::SoundError};
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
/// See module docs.
#[derive(Debug)]
pub enum EngineError {
/// Sound system error.
Sound(SoundError),
/// Rendering system error.
Renderer(FrameworkError),
/// Internal error.
Custom(String),
}
impl std::error::Error for EngineError {}
impl Display for EngineError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EngineError::Sound(v) => Display::fmt(v, f),
EngineError::Renderer(v) => Display::fmt(v, f),
EngineError::Custom(v) => {
write!(f, "Custom error: {v}")
}
}
}
}
impl From<SoundError> for EngineError {
fn from(sound: SoundError) -> Self {
Self::Sound(sound)
}
}
impl From<FrameworkError> for EngineError {
fn from(renderer: FrameworkError) -> Self {
Self::Renderer(renderer)
}
}
impl From<Box<dyn Error>> for EngineError {
fn from(e: Box<dyn Error>) -> Self {
Self::Custom(format!("{e:?}"))
}
}
+536
View File
@@ -0,0 +1,536 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Executor is a small wrapper that manages plugins and scripts for your game.
use crate::engine::ApplicationLoopController;
use crate::scene::Scene;
use crate::{
asset::manager::ResourceManager,
core::{
instant::Instant,
log::{Log, MessageKind},
task::TaskPool,
},
engine::{
Engine, EngineInitParams, GraphicsContext, GraphicsContextParams, SerializationContext,
},
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
plugin::Plugin,
utils::translate_event,
window::WindowAttributes,
};
use clap::Parser;
use fyrox_core::pool::Handle;
use fyrox_resource::io::FsResourceIo;
use fyrox_ui::constructor::new_widget_constructor_container;
use std::cell::Cell;
use std::collections::VecDeque;
use std::time::Duration;
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use winit::event_loop::ActiveEventLoop;
#[derive(Parser, Debug, Default)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, default_value = None)]
override_scene: Option<String>,
}
/// Executor is a small wrapper that manages plugins and scripts for your game.
pub struct Executor {
event_loop: Option<EventLoop<()>>,
engine: Engine,
desired_update_rate: f32,
throttle_threshold: f32,
throttle_frame_interval: usize,
resource_hot_reloading: bool,
}
impl Deref for Executor {
type Target = Engine;
fn deref(&self) -> &Self::Target {
&self.engine
}
}
impl DerefMut for Executor {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.engine
}
}
impl Executor {
/// Default update rate in frames per second.
pub const DEFAULT_UPDATE_RATE: f32 = 60.0;
/// Default time step (in seconds).
pub const DEFAULT_TIME_STEP: f32 = 1.0 / Self::DEFAULT_UPDATE_RATE;
/// Creates new game executor using specified set of parameters. Much more flexible version of
/// [`Executor::new`]. To run the engine in headless mode, pass [`None`] to the `event_loop`
/// argument.
pub fn from_params(
event_loop: Option<EventLoop<()>>,
graphics_context_params: GraphicsContextParams,
) -> Self {
let serialization_context = Arc::new(SerializationContext::new());
let task_pool = Arc::new(TaskPool::new());
let io = Arc::new(FsResourceIo);
let engine = Engine::new(EngineInitParams {
graphics_context_params,
resource_manager: ResourceManager::new(io, task_pool.clone()),
serialization_context,
task_pool,
widget_constructors: Arc::new(new_widget_constructor_container()),
dyn_type_constructors: Default::default(),
})
.unwrap();
Self {
event_loop,
engine,
desired_update_rate: Self::DEFAULT_UPDATE_RATE,
throttle_threshold: 2.0 * Self::DEFAULT_TIME_STEP,
throttle_frame_interval: 5,
resource_hot_reloading: true,
}
}
/// Creates new game executor using default window and with vsync turned on. For more flexible
/// way to create an executor see [`Executor::from_params`]. To run the engine in headless mode,
/// pass [`None`] to the `event_loop` argument.
pub fn new(event_loop: Option<EventLoop<()>>) -> Self {
let mut window_attributes = WindowAttributes::default();
window_attributes.resizable = true;
window_attributes.title = "Fyrox Game".to_string();
Self::from_params(
event_loop,
GraphicsContextParams {
window_attributes,
vsync: true,
msaa_sample_count: None,
graphics_server_constructor: Default::default(),
named_objects: false,
},
)
}
/// Enables or disables hot reloading of changed resources (such as textures, shaders, scenes, etc.).
/// Enabled by default.
///
/// # Platform-specific
///
/// Does nothing on Android and WebAssembly, because these OSes does not have rich file system
/// as PC.
pub fn set_resource_hot_reloading_enabled(&mut self, enabled: bool) {
self.resource_hot_reloading = enabled;
}
/// Returns `true` if hot reloading of changed resources is enabled, `false` - otherwise.
pub fn is_resource_hot_reloading_enabled(&self) -> bool {
self.resource_hot_reloading
}
/// Sets the desired throttle threshold (in seconds), at which the engine will stop trying to
/// stabilize the update rate of the game logic and will increase the time step. This option
/// could be useful to prevent potential hang up of the game if its logic or rendering takes too
/// much time at each frame. The default value is two default time steps (33.3(3) milliseconds
/// or 0.0333(3) seconds).
///
/// ## Important notes
///
/// Physics could suffer from variable time step which may result in objects falling through the
/// ground and some other nasty things. Throttle threshold should be at reasonably high levels
/// (usually 2x-3x of the fixed time step).
pub fn set_throttle_threshold(&mut self, threshold: f32) {
self.throttle_threshold = threshold.max(0.001);
}
/// Returns current throttle threshold. See [`Self::set_throttle_threshold`] docs for more info.
pub fn throttle_threshold(&self) -> f32 {
self.throttle_threshold
}
/// Sets the amount of frames (consecutive) that will be allowed to have lag spikes and the engine
/// won't modify time step for internal update calls during such interval. This setting allows the
/// engine to ignore small lag spikes and do not fast-forward game logic using variable time step.
/// Variable time step could be bad for physics, which may result in objects falling through the
/// ground, etc. Default is 5 frames.
pub fn set_throttle_frame_interval(&mut self, interval: usize) {
self.throttle_frame_interval = interval;
}
/// Returns current throttle frame interval. See [`Self::set_throttle_frame_interval`] docs for
/// more info.
pub fn throttle_frame_interval(&self) -> usize {
self.throttle_frame_interval
}
/// Sets the desired update rate in frames per second.
pub fn set_desired_update_rate(&mut self, update_rate: f32) {
self.desired_update_rate = update_rate.abs();
}
/// Returns desired update rate in frames per second.
pub fn desired_update_rate(&self) -> f32 {
self.desired_update_rate
}
/// Adds new plugin to the executor, the plugin will be enabled only on [`Executor::run`].
pub fn add_plugin<P>(&mut self, plugin: P)
where
P: Plugin + 'static,
{
self.engine.add_plugin(plugin)
}
/// Runs the executor - starts your game.
pub fn run(self) {
Log::info("Initializing resource registry.");
self.engine.resource_manager.update_or_load_registry();
let engine = self.engine;
let event_loop = self.event_loop;
let throttle_threshold = self.throttle_threshold;
let throttle_frame_interval = self.throttle_frame_interval;
if self.resource_hot_reloading {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
{
use crate::core::watcher::FileSystemWatcher;
use std::time::Duration;
match FileSystemWatcher::new(".", Duration::from_secs(1)) {
Ok(watcher) => {
engine.resource_manager.state().set_watcher(Some(watcher));
}
Err(e) => {
Log::err(format!("Unable to create resource watcher. Reason {e:?}"));
}
}
}
}
let args = Args::try_parse().unwrap_or_default();
match event_loop {
Some(event_loop) => run_normal(
engine,
args.override_scene.as_deref(),
event_loop,
throttle_threshold,
throttle_frame_interval,
self.desired_update_rate,
),
None => run_headless(
engine,
args.override_scene.as_deref(),
throttle_threshold,
throttle_frame_interval,
self.desired_update_rate,
),
}
}
}
fn run_headless(
mut engine: Engine,
override_scene: Option<&str>,
throttle_threshold: f32,
throttle_frame_interval: usize,
desired_update_rate: f32,
) {
let mut previous = Instant::now();
let fixed_time_step = 1.0 / desired_update_rate;
let mut lag = fixed_time_step;
let mut frame_counter = 0usize;
let mut last_throttle_frame_number = 0usize;
let is_running = Cell::new(true);
while is_running.get() {
if !engine.plugins_enabled && engine.resource_manager.registry_is_loaded() {
engine.enable_plugins(
override_scene,
true,
ApplicationLoopController::Headless {
running: &is_running,
},
);
}
register_scripted_scenes(&mut engine);
game_loop_iteration(
&mut engine,
ApplicationLoopController::Headless {
running: &is_running,
},
&mut previous,
&mut lag,
fixed_time_step,
throttle_threshold,
throttle_frame_interval,
frame_counter,
&mut last_throttle_frame_number,
);
frame_counter += 1;
// Only sleep for two-third of the remaining time step because thread::sleep tends to overshoot.
let sleep_time = (fixed_time_step - previous.elapsed().as_secs_f32()).max(0.0) * 0.66666;
if sleep_time > 0.0 {
std::thread::sleep(Duration::from_secs_f32(sleep_time));
}
}
}
fn run_normal(
mut engine: Engine,
override_scene: Option<&str>,
event_loop: EventLoop<()>,
throttle_threshold: f32,
throttle_frame_interval: usize,
desired_update_rate: f32,
) {
let mut previous = Instant::now();
let fixed_time_step = 1.0 / desired_update_rate;
let mut lag = 0.0;
let mut frame_counter = 0usize;
let mut last_throttle_frame_number = 0usize;
let override_scene = override_scene.map(|s| s.to_string());
enum GraphicsEvent {
GraphicsContextInitialized,
GraphicsContextDestroyed,
}
let mut graphics_event_queue = VecDeque::new();
run_executor(event_loop, move |event, active_event_loop| {
active_event_loop.set_control_flow(ControlFlow::Wait);
engine.handle_os_events(
&event,
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
if !engine.plugins_enabled && engine.resource_manager.registry_is_loaded() {
engine.enable_plugins(
override_scene.as_deref(),
true,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
);
while let Some(graphics_event) = graphics_event_queue.pop_front() {
match graphics_event {
GraphicsEvent::GraphicsContextInitialized => {
engine.handle_graphics_context_created_by_plugins(
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
}
GraphicsEvent::GraphicsContextDestroyed => {
engine.handle_graphics_context_destroyed_by_plugins(
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
}
}
}
}
let scripted_scenes = register_scripted_scenes(&mut engine);
for scripted_scene in scripted_scenes {
engine.handle_os_event_by_scripts(&event, scripted_scene, fixed_time_step);
}
match event {
Event::Resumed => {
engine
.initialize_graphics_context(active_event_loop)
.expect("Unable to initialize graphics context!");
if engine.plugins_enabled {
engine.handle_graphics_context_created_by_plugins(
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
} else {
graphics_event_queue.push_back(GraphicsEvent::GraphicsContextInitialized);
}
}
Event::Suspended => {
engine
.destroy_graphics_context()
.expect("Unable to destroy graphics context!");
if engine.plugins_enabled {
engine.handle_graphics_context_destroyed_by_plugins(
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
} else {
graphics_event_queue.push_back(GraphicsEvent::GraphicsContextDestroyed);
}
}
Event::AboutToWait => {
game_loop_iteration(
&mut engine,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut previous,
&mut lag,
fixed_time_step,
throttle_threshold,
throttle_frame_interval,
frame_counter,
&mut last_throttle_frame_number,
);
}
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::CloseRequested => active_event_loop.exit(),
WindowEvent::Resized(size) => {
if let Err(e) = engine.set_frame_size(size.into()) {
Log::writeln(
MessageKind::Error,
format!("Unable to set frame size: {e:?}"),
);
}
}
WindowEvent::RedrawRequested => {
engine.handle_before_rendering_by_plugins(
fixed_time_step,
ApplicationLoopController::ActiveEventLoop(active_event_loop),
&mut lag,
);
engine.render().unwrap();
frame_counter += 1;
}
_ => (),
}
if let Some(os_event) = translate_event(&event) {
for ui in engine.user_interfaces.iter_mut() {
ui.process_os_event(&os_event);
}
}
}
_ => (),
}
})
}
fn register_scripted_scenes(engine: &mut Engine) -> Vec<Handle<Scene>> {
let scenes = engine
.scenes
.pair_iter()
.map(|(s, _)| s)
.collect::<Vec<_>>();
for &scene_handle in scenes.iter() {
if !engine.has_scripted_scene(scene_handle) {
engine.register_scripted_scene(scene_handle);
}
}
scenes
}
fn game_loop_iteration(
engine: &mut Engine,
controller: ApplicationLoopController,
previous: &mut Instant,
lag: &mut f32,
fixed_time_step: f32,
throttle_threshold: f32,
throttle_frame_interval: usize,
frame_counter: usize,
last_throttle_frame_number: &mut usize,
) {
let elapsed = previous.elapsed();
*previous = Instant::now();
*lag += elapsed.as_secs_f32();
// Update rate stabilization loop.
while *lag >= fixed_time_step {
let time_step;
if *lag >= throttle_threshold
&& (frame_counter - *last_throttle_frame_number >= throttle_frame_interval)
{
// Modify the delta time to let the game internals to fast-forward the
// logic by the current lag.
time_step = *lag;
// Reset the lag to exit early from the loop, thus preventing its
// potential infinite increase, that in its turn could hang up the game.
*lag = 0.0;
*last_throttle_frame_number = frame_counter;
} else {
time_step = fixed_time_step;
}
engine.update(time_step, controller, lag, Default::default());
// Additional check is needed, because the `update` call above could modify
// the lag.
if *lag >= fixed_time_step {
*lag -= fixed_time_step;
} else if *lag < 0.0 {
// Prevent from going back in time.
*lag = 0.0;
}
}
if let GraphicsContext::Initialized(ref ctx) = engine.graphics_context {
ctx.window.request_redraw();
}
}
#[allow(deprecated)] // TODO
fn run_executor<F>(event_loop: EventLoop<()>, callback: F)
where
F: FnMut(Event<()>, &ActiveEventLoop) + 'static,
{
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::EventLoopExtWebSys;
event_loop.spawn(callback);
}
#[cfg(not(target_arch = "wasm32"))]
{
event_loop.run(callback).unwrap();
}
}
+367
View File
@@ -0,0 +1,367 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::{
dyntype::{DynTypeConstructorContainer, DynTypeContainer},
log::Log,
pool::{Handle, PayloadContainer, Ticket},
visitor::{error::VisitError, Visit, Visitor, VisitorFlags},
},
engine::SerializationContext,
graph::SceneGraph,
gui::constructor::WidgetConstructorContainer,
plugin::Plugin,
resource::model::ModelResource,
scene::{
base::{visit_opt_script, NodeMessage, NodeScriptMessage},
node::{container::NodeContainer, Node},
Scene,
},
script::Script,
};
use std::{
ops::Deref,
sync::{mpsc::Sender, Arc},
};
pub struct ScriptState {
index: usize,
binary_blob: Vec<u8>,
}
pub struct NodeState {
node: Handle<Node>,
ticket: Option<Ticket<Node>>,
binary_blob: Vec<u8>,
scripts: Vec<ScriptState>,
}
pub struct SceneState {
pub scene: Handle<Scene>,
nodes: Vec<NodeState>,
scene_user_data_blob: Vec<u8>,
}
fn serialize_user_data(scene: &mut Scene) -> Result<Vec<u8>, String> {
let mut visitor = make_writing_visitor();
scene
.graph
.user_data
.visit("UserData", &mut visitor)
.map_err(|e| e.to_string())?;
visitor.save_binary_to_vec().map_err(|e| e.to_string())
}
fn deserialize_user_data(
scene: &mut Scene,
user_data_blob: &[u8],
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
widget_constructors: &Arc<WidgetConstructorContainer>,
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
) -> Result<(), String> {
let mut visitor = make_reading_visitor(
user_data_blob,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)
.map_err(|e| e.to_string())?;
let mut container = DynTypeContainer::default();
container
.visit("UserData", &mut visitor)
.map_err(|e| e.to_string())?;
scene.graph.user_data = container;
Ok(())
}
impl SceneState {
pub fn try_create_from_plugin(
scene_handle: Handle<Scene>,
scene: &mut Scene,
serialization_context: &SerializationContext,
plugin: &dyn Plugin,
) -> Result<Option<Self>, String> {
let mut scene_state = Self {
scene_user_data_blob: serialize_user_data(scene)?,
scene: scene_handle,
nodes: Default::default(),
};
for index in 0..scene.graph.capacity() {
let handle = scene.graph.handle_from_index(index);
let Ok(node) = scene.graph.try_get_node_mut(handle) else {
continue;
};
let mut node_state = NodeState {
node: handle,
ticket: None,
binary_blob: Default::default(),
scripts: Default::default(),
};
if is_node_belongs_to_plugin(serialization_context, node, plugin) {
// The entire node belongs to plugin, serialize it entirely.
// Take the node out of the graph first.
let (ticket, node) = scene.graph.take_reserve(handle);
let mut container = NodeContainer::new(node);
let mut visitor = make_writing_visitor();
container
.visit("Node", &mut visitor)
.map_err(|e| e.to_string())?;
node_state.binary_blob = visitor.save_binary_to_vec().map_err(|e| e.to_string())?;
node_state.ticket = Some(ticket);
} else {
// The node does not belong to the plugin, try to check its scripts.
for (script_index, record) in node.scripts.iter_mut().enumerate() {
if let Some(script) = record.script.as_ref() {
if is_script_belongs_to_plugin(serialization_context, script, plugin) {
// Take the script out of the node and serialize it. The script will be
// dropped and destroyed.
let mut script = record.script.take();
let mut visitor = make_writing_visitor();
visit_opt_script("Script", &mut script, &mut visitor)
.map_err(|e| e.to_string())?;
let binary_blob =
visitor.save_binary_to_vec().map_err(|e| e.to_string())?;
node_state.scripts.push(ScriptState {
index: script_index,
binary_blob,
})
}
}
}
}
if !node_state.binary_blob.is_empty() || !node_state.scripts.is_empty() {
scene_state.nodes.push(node_state);
}
}
if !scene_state.nodes.is_empty() {
Ok(Some(scene_state))
} else {
Ok(None)
}
}
pub fn deserialize_into_scene(
self,
scene: &mut Scene,
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
widget_constructors: &Arc<WidgetConstructorContainer>,
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
) -> Result<(), String> {
deserialize_user_data(
scene,
&self.scene_user_data_blob,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)?;
// SAFETY: Scene is guaranteed to be used only once per inner loop.
let scene2 = unsafe { &mut *(scene as *mut Scene) };
let script_message_sender = scene.graph.script_message_sender.clone();
let message_sender = scene.graph.message_sender.clone();
self.deserialize_into_scene_internal(
|handle: Handle<Node>, index, script| {
scene.graph[handle].scripts[index].script = script;
},
|handle: Handle<Node>, node| {
scene2.graph[handle] = node;
},
script_message_sender,
message_sender,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)
}
pub fn deserialize_into_prefab_scene(
self,
prefab: &ModelResource,
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
widget_constructors: &Arc<WidgetConstructorContainer>,
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
) -> Result<(), String> {
deserialize_user_data(
&mut prefab.data_ref().scene,
&self.scene_user_data_blob,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)?;
let script_message_sender = prefab.data_ref().scene.graph.script_message_sender.clone();
let message_sender = prefab.data_ref().scene.graph.message_sender.clone();
self.deserialize_into_scene_internal(
|handle: Handle<Node>, index, script| {
prefab.data_ref().scene.graph[handle].scripts[index].script = script;
},
|handle: Handle<Node>, node| {
prefab.data_ref().scene.graph[handle] = node;
},
script_message_sender,
message_sender,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)
}
pub fn deserialize_into_scene_internal<S, N>(
self,
mut set_script: S,
mut set_node: N,
script_message_sender: Sender<NodeScriptMessage>,
message_sender: Sender<NodeMessage>,
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
widget_constructors: &Arc<WidgetConstructorContainer>,
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
) -> Result<(), String>
where
S: FnMut(Handle<Node>, usize, Option<Script>),
N: FnMut(Handle<Node>, Node),
{
for node_state in self.nodes {
if node_state.binary_blob.is_empty() {
// Only scripts needs to be reloaded.
for script in node_state.scripts {
let mut visitor = make_reading_visitor(
&script.binary_blob,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)
.map_err(|e| e.to_string())?;
let mut opt_script: Option<Script> = None;
visit_opt_script("Script", &mut opt_script, &mut visitor)
.map_err(|e| e.to_string())?;
set_script(node_state.node, script.index, opt_script);
Log::info(format!(
"Script {} of node {} was successfully deserialized.",
script.index, node_state.node
));
}
} else {
let mut visitor = make_reading_visitor(
&node_state.binary_blob,
serialization_context,
resource_manager,
widget_constructors,
dyn_type_constructors,
)
.map_err(|e| e.to_string())?;
let mut container = NodeContainer::default();
container
.visit("Node", &mut visitor)
.map_err(|e| e.to_string())?;
if let Some(mut new_node) = container.take() {
new_node.on_connected_to_graph(
node_state.node,
message_sender.clone(),
script_message_sender.clone(),
);
set_node(node_state.node, new_node);
Log::info(format!(
"Node {} was successfully deserialized.",
node_state.node
));
}
}
}
Ok(())
}
}
pub fn make_writing_visitor() -> Visitor {
let mut visitor = Visitor::new();
visitor.flags = VisitorFlags::SERIALIZE_EVERYTHING;
visitor
}
pub fn make_reading_visitor(
binary_blob: &[u8],
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
widget_constructors: &Arc<WidgetConstructorContainer>,
dyn_type_constructors: &Arc<DynTypeConstructorContainer>,
) -> Result<Visitor, VisitError> {
let mut visitor = Visitor::load_from_memory(binary_blob)?;
visitor.blackboard.register(serialization_context.clone());
visitor
.blackboard
.register(Arc::new(resource_manager.clone()));
visitor.blackboard.register(widget_constructors.clone());
visitor.blackboard.register(dyn_type_constructors.clone());
Ok(visitor)
}
fn is_script_belongs_to_plugin(
serialization_context: &SerializationContext,
script: &Script,
plugin: &dyn Plugin,
) -> bool {
let script_id = script.deref().id();
if let Some(constructor) = serialization_context
.script_constructors
.map()
.get(&script_id)
{
if constructor.assembly_name == plugin.type_info_ref().assembly_name {
return true;
}
}
false
}
fn is_node_belongs_to_plugin(
serialization_context: &SerializationContext,
node: &Node,
plugin: &dyn Plugin,
) -> bool {
let node_id = (*node).id();
if let Some(constructor) = serialization_context.node_constructors.map().get(&node_id) {
if constructor.assembly_name == plugin.type_info_ref().assembly_name {
return true;
}
}
false
}
+247
View File
@@ -0,0 +1,247 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! This module contains a set of "shortcuts" that allows getting the state of a mouse and a keyboard
//! in a simplified manner (read - without using "verbose" event-based approach). It may be useful
//! in simple scenarios where you just need to know if a button (on keyboard, mouse) was pressed
//! and do something. You should always prefer the event-based approach when possible.
use fxhash::{FxHashMap, FxHashSet};
use fyrox_core::algebra::Vector2;
use winit::event::{ButtonId, ElementState};
use winit::keyboard::{KeyCode, PhysicalKey};
/// Represents the mouse state in the current frame. The contents of this structure is a simplified
/// version of event-based approach. **Important:** this structure does not track from which mouse the
/// corresponding event has come from, if you have more than one mouse use event-based approach
/// instead!
#[derive(Default, Clone)]
pub struct Mouse {
/// Coordinates in pixels relative to the top-left corner of the window.
pub position: Vector2<f32>,
/// Speed of the mouse in some units.
pub speed: Vector2<f32>,
/// Physical state of mouse buttons. Usually, the button indices are the following:
///
/// - 0 - left mouse button
/// - 1 - right mouse button
/// - 2 - middle mouse button
/// - 3 - additional mouse button (could back or forward)
/// - 4 - additional mouse button (could back or forward)
/// - 5 and higher - device-specific buttons
///
/// There are named constants for the tree most used ones - [`Mouse::LEFT_BUTTON`],
/// [`Mouse::RIGHT_BUTTON`], [`Mouse::MIDDLE_BUTTON`].
pub buttons_state: FxHashMap<ButtonId, ElementState>,
/// A hash set that contains all the buttons that were pressed in the current frame. If a button
/// is still pressed in the next frame, this hash map will not contain it. This is useful
/// to check if a button was pressed and some action, but do not repeat the same action over and
/// over until the button is released.
pub pressed_buttons: FxHashSet<ButtonId>,
/// A hash set that contains all the buttons that were released in the current frame. If a button
/// is still released in the next frame, this hash map will not contain it. This is useful
/// to check if a button was released and some action, but do not repeat the same action over and
/// over until the button is pressed.
pub released_buttons: FxHashSet<ButtonId>,
}
impl Mouse {
/// Index of the left button.
pub const LEFT_BUTTON: ButtonId = 0;
/// Index of the right button.
pub const RIGHT_BUTTON: ButtonId = 1;
/// Index of the middle button.
pub const MIDDLE_BUTTON: ButtonId = 2;
}
/// Represents the keyboard state in the current frame. The contents of this structure is a simplified
/// version of event-based approach. **Important:** this structure does not track from which keyboard the
/// corresponding event has come from, if you have more than one keyboard use event-based approach
/// instead!
#[derive(Default, Clone)]
pub struct Keyboard {
/// Represents the keyboard state in the current frame.
pub keys: FxHashMap<PhysicalKey, ElementState>,
/// A hash set that contains all the keys that were pressed in the current frame. If a key
/// is still pressed in the next frame, this hash map will not contain it. This is useful
/// to check if a key was pressed and some action, but do not repeat the same action over and
/// over until the key is released.
pub pressed_keys: FxHashSet<PhysicalKey>,
/// A hash set that contains all the keys that were released in the current frame. If a key
/// is still released in the next frame, this hash map will not contain it. This is useful
/// to check if a key was released and some action, but do not repeat the same action over and
/// over until the key is pressed.
pub released_keys: FxHashSet<PhysicalKey>,
}
/// A stored state of most common input events. It is used a "shortcut" in cases where event-based
/// approach is too verbose. **Important:** this structure does not track from which device the
/// corresponding event has come from, if you have more than one keyboard and/or mouse, use
/// event-based approach instead! You should always prefer the event-based approach when possible.
#[derive(Default, Clone)]
pub struct InputState {
/// Represents the mouse state in the current frame.
pub mouse: Mouse,
/// Represents the keyboard state in the current frame.
pub keyboard: Keyboard,
}
impl InputState {
/// Returns `true` if the specified key is pressed, `false` - otherwise.
#[inline]
pub fn is_key_down(&self, key: KeyCode) -> bool {
self.keyboard
.keys
.get(&PhysicalKey::Code(key))
.is_some_and(|state| *state == ElementState::Pressed)
}
/// Returns `true` if the specified key was pressed in the current frame, `false` - otherwise.
/// This method will return `false` if the key is still pressed in the next frame. This is
/// useful to check if a key was pressed and some action, but do not repeat the same action
/// over and over until the key is released.
#[inline]
pub fn is_key_pressed(&self, key: KeyCode) -> bool {
self.keyboard.pressed_keys.contains(&PhysicalKey::Code(key))
}
/// Returns `true` if the specified key was released in the current frame, `false` - otherwise.
/// This method will return `false` if the key is still released in the next frame. This is
/// useful to check if a key was released and some action, but do not repeat the same action
/// over and over until the key is pressed.
#[inline]
pub fn is_key_released(&self, key: KeyCode) -> bool {
self.keyboard
.released_keys
.contains(&PhysicalKey::Code(key))
}
/// Returns `true` if the specified mouse button is pressed, `false` - otherwise. Usually,
/// the button indices are the following:
///
/// - 0 - left mouse button
/// - 1 - right mouse button
/// - 2 - middle mouse button
/// - 3 - additional mouse button (could back or forward)
/// - 4 - additional mouse button (could back or forward)
/// - 5 and higher - device-specific buttons
///
/// There are named constants for the tree most used ones - [`Mouse::LEFT_BUTTON`],
/// [`Mouse::RIGHT_BUTTON`], [`Mouse::MIDDLE_BUTTON`].
#[inline]
pub fn is_mouse_button_down(&self, button_id: ButtonId) -> bool {
self.mouse
.buttons_state
.get(&button_id)
.is_some_and(|state| *state == ElementState::Pressed)
}
/// Returns `true` if the specified button was pressed in the current frame, `false` - otherwise.
/// This method will return `false` if the button is still pressed in the next frame. This is
/// useful to check if a button was pressed and some action, but do not repeat the same action
/// over and over until the button is released. See the docs of [`Self::is_mouse_button_down`]
/// for button ids.
#[inline]
pub fn is_mouse_button_pressed(&self, button_id: ButtonId) -> bool {
self.mouse.pressed_buttons.contains(&button_id)
}
/// Returns `true` if the specified button was released in the current frame, `false` - otherwise.
/// This method will return `false` if the button is still released in the next frame. This is
/// useful to check if a button was released and some action, but do not repeat the same action
/// over and over until the button is pressed. See the docs of [`Self::is_mouse_button_down`]
/// for button ids.
#[inline]
pub fn is_mouse_button_released(&self, button_id: ButtonId) -> bool {
self.mouse.released_buttons.contains(&button_id)
}
/// Returns `true` if the left mouse button is pressed, `false` - otherwise.
#[inline]
pub fn is_left_mouse_button_down(&self) -> bool {
self.is_mouse_button_down(Mouse::LEFT_BUTTON)
}
/// Returns `true` if the right mouse button is pressed, `false` - otherwise.
#[inline]
pub fn is_right_mouse_button_down(&self) -> bool {
self.is_mouse_button_down(Mouse::RIGHT_BUTTON)
}
/// Returns `true` if the middle mouse button pressed is pressed, `false` - otherwise.
#[inline]
pub fn is_middle_mouse_button_down(&self) -> bool {
self.is_mouse_button_down(Mouse::MIDDLE_BUTTON)
}
/// Returns `true` if the left mouse button was pressed on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_left_mouse_button_pressed(&self) -> bool {
self.is_mouse_button_pressed(Mouse::LEFT_BUTTON)
}
/// Returns `true` if the right mouse button was pressed on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_right_mouse_button_pressed(&self) -> bool {
self.is_mouse_button_pressed(Mouse::RIGHT_BUTTON)
}
/// Returns `true` if the middle mouse button was pressed on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_middle_mouse_button_pressed(&self) -> bool {
self.is_mouse_button_pressed(Mouse::MIDDLE_BUTTON)
}
/// Returns `true` if the left mouse button was released on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_left_mouse_button_released(&self) -> bool {
self.is_mouse_button_released(Mouse::LEFT_BUTTON)
}
/// Returns `true` if the right mouse button was released on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_right_mouse_button_released(&self) -> bool {
self.is_mouse_button_released(Mouse::RIGHT_BUTTON)
}
/// Returns `true` if the middle mouse button was released on the current frame,
/// `false` - otherwise.
#[inline]
pub fn is_middle_mouse_button_released(&self) -> bool {
self.is_mouse_button_released(Mouse::MIDDLE_BUTTON)
}
/// Returns mouse speed in the current frame, the speed expressed in some arbitrary units.
#[inline]
pub fn mouse_speed(&self) -> Vector2<f32> {
self.mouse.speed
}
/// Returns mouse position in pixels relative to the top-left corner of the main window.
#[inline]
pub fn mouse_position(&self) -> Vector2<f32> {
self.mouse.position
}
}
File diff suppressed because it is too large Load Diff
+262
View File
@@ -0,0 +1,262 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Asynchronous task handler. See [`TaskPoolHandler`] for more info and usage examples.
use crate::plugin::error::GameResult;
use crate::plugin::PluginContainer;
use crate::{
core::{
pool::Handle,
task::{AsyncTask, AsyncTaskResult, TaskPool},
uuid::Uuid,
},
plugin::{Plugin, PluginContext},
scene::{node::Node, Scene},
script::{ScriptContext, ScriptTrait},
};
use fxhash::FxHashMap;
use std::sync::Arc;
pub(crate) type NodeTaskHandlerClosure = Box<
dyn for<'a, 'b, 'c> FnOnce(
Box<dyn AsyncTaskResult>,
&mut dyn ScriptTrait,
&mut ScriptContext<'a, 'b, 'c>,
) -> GameResult,
>;
pub(crate) type PluginTaskHandler = Box<
dyn for<'a, 'b> FnOnce(
Box<dyn AsyncTaskResult>,
&'a mut [PluginContainer],
&mut PluginContext<'a, 'b>,
) -> GameResult,
>;
pub(crate) struct NodeTaskHandler {
pub(crate) scene_handle: Handle<Scene>,
pub(crate) node_handle: Handle<Node>,
pub(crate) script_index: usize,
pub(crate) closure: NodeTaskHandlerClosure,
}
/// Asynchronous task handler is used as an executor for async functions (tasks), that in addition to them
/// has a closure, that will be called when a task is finished. The main use case for such tasks is to
/// off-thread a heavy task to one of background threads (from a thread pool on PC, or a microtask on
/// WebAssembly) and when it is done, incorporate its result in your game's state. A task and its
/// "on-complete" closure could be pretty much anything: procedural world generation + adding a
/// generated scene to the engine, asset loading + its instantiation, etc. It should be noted that
/// a task itself is executed asynchronously (in other thread), while a closure - synchronously,
/// just at the beginning of the next game loop iteration. This means, that you should never put
/// heavy tasks into the closure, otherwise it will result in quite notable stutters.
///
/// There are two main methods - [`TaskPoolHandler::spawn_plugin_task`] and [`TaskPoolHandler::spawn_script_task`].
/// They are somewhat similar, but the main difference between them is that the first one operates
/// on per plugin basis and the latter operates on scene node basis. This means that in case of
/// [`TaskPoolHandler::spawn_plugin_task`], it will accept an async task and when it is finished, it
/// will give you a result of the task and access to the plugin from which it was called, so you can
/// do some actions with the result. [`TaskPoolHandler::spawn_script_task`] does somewhat the same, but
/// on a scene node basis - when a task is done, the "on-complete" closure will be provided with a
/// wide context, allowing you to modify the caller's node state. See the docs for the respective
/// methods for more info.
pub struct TaskPoolHandler {
task_pool: Arc<TaskPool>,
plugin_task_handlers: FxHashMap<Uuid, PluginTaskHandler>,
node_task_handlers: FxHashMap<Uuid, NodeTaskHandler>,
}
impl TaskPoolHandler {
pub(crate) fn new(task_pool: Arc<TaskPool>) -> Self {
Self {
task_pool,
plugin_task_handlers: Default::default(),
node_task_handlers: Default::default(),
}
}
/// Spawns a task represented by the `future`, that does something and then adds the result to
/// a plugin it was called from using the `on_complete` closure.
///
/// ## Example
///
/// ```rust ,no_run
/// # use fyrox_impl::plugin::{Plugin, PluginContext, error::GameResult};
/// # use fyrox_impl::core::visitor::prelude::*;
/// # use fyrox_impl::core::reflect::prelude::*;
/// # use std::{fs::File, io::Read};
///
/// #[derive(Visit, Reflect, PartialEq, Debug)]
/// #[reflect(non_cloneable, type_uuid = "a7ba17c1-6104-4938-9ba7-ba479acf716a")]
/// struct MyGame {
/// data: Option<Vec<u8>>,
/// }
///
/// impl MyGame {
/// pub fn new(context: PluginContext) -> Self {
/// context.task_pool.spawn_plugin_task(
/// // Emulate heavy task by reading a potentially large file. The game will be fully
/// // responsive while it runs.
/// async move {
/// let mut file = File::open("some/file.txt").unwrap();
/// let mut data = Vec::new();
/// file.read_to_end(&mut data).unwrap();
/// data
/// },
/// // This closure is called when the future above has finished, but not immediately - on
/// // the next update iteration.
/// |data, game: &mut MyGame, _context| {
/// // Store the data in the game instance.
/// game.data = Some(data);
/// Ok(())
/// },
/// );
///
/// // Immediately return the new game instance with empty data.
/// Self { data: None }
/// }
/// }
///
/// impl Plugin for MyGame {
/// fn update(&mut self, _context: &mut PluginContext) -> GameResult {
/// // Do something with the data.
/// if let Some(data) = self.data.take() {
/// println!("The data is: {:?}", data);
/// }
/// Ok(())
/// }
/// }
/// ```
#[inline]
pub fn spawn_plugin_task<F, T, P, C>(&mut self, future: F, on_complete: C)
where
F: AsyncTask<T>,
T: AsyncTaskResult,
P: Plugin,
for<'a, 'b> C: FnOnce(T, &mut P, &mut PluginContext<'a, 'b>) -> GameResult + 'static,
{
let task_id = self.task_pool.spawn_with_result(future);
self.plugin_task_handlers.insert(
task_id,
Box::new(move |result, plugins, context| {
let plugin = plugins
.iter_mut()
.find_map(|p| p.cast_mut::<P>())
.expect("Plugin must be present!");
let typed = result.downcast::<T>().expect("Types must match!");
on_complete(*typed, plugin, context)
}),
);
}
/// Spawns a task represented by the `future`, that does something and then adds the result to
/// a scene node's script using the `on_complete` closure. This method could be used to off-thread some
/// heavy work from usual update routine (for example - pathfinding).
///
/// ## Examples
///
/// ```rust ,no_run
/// # use fyrox_impl::{
/// # core::{reflect::prelude::*, uuid::Uuid, visitor::prelude::*},
/// # resource::model::{Model, ModelResourceExtension},
/// # script::{ScriptContext, ScriptTrait},
/// # };
/// #
/// # use fyrox_impl::plugin::error::GameResult;
/// #
/// #[derive(Reflect, Visit, PartialEq, Default, Debug, Clone)]
/// #[reflect(type_uuid = "f5ded79e-6101-4e23-b20d-48cbdb25d87a")]
/// struct MyScript;
///
/// impl ScriptTrait for MyScript {
/// fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
/// ctx.task_pool.spawn_script_task(
/// ctx.scene_handle,
/// ctx.handle,
/// ctx.script_index,
/// // Request loading of some heavy asset. It does not actually does the loading in the
/// // same routine, since asset loading itself is asynchronous, but we can't block the
/// // current thread on all support platforms to wait until the loading is done. So we
/// // have to use this approach to load assets on demand. Since every asset implements
/// // Future trait, it can be used directly as a future. Alternatively, you can use async
/// // move { } block here.
/// ctx.resource_manager.request::<Model>("path/to/model.fbx"),
/// // This closure will executed only when the upper future is done and only on the next
/// // update iteration.
/// |result, script: &mut MyScript, ctx| {
/// result?.instantiate(&mut ctx.scene);
/// Ok(())
/// },
/// );
/// Ok(())
/// }
/// }
/// ```
#[inline]
pub fn spawn_script_task<F, T, C, S>(
&mut self,
scene_handle: Handle<Scene>,
node_handle: Handle<Node>,
script_index: usize,
future: F,
on_complete: C,
) where
F: AsyncTask<T>,
T: AsyncTaskResult,
for<'a, 'b, 'c> C:
FnOnce(T, &mut S, &mut ScriptContext<'a, 'b, 'c>) -> GameResult + 'static,
S: ScriptTrait,
{
let task_id = self.task_pool.spawn_with_result(future);
self.node_task_handlers.insert(
task_id,
NodeTaskHandler {
scene_handle,
node_handle,
script_index,
closure: Box::new(move |result, script, context| {
let script = script
.as_any_ref_mut()
.downcast_mut::<S>()
.expect("Types must match");
let result = result.downcast::<T>().expect("Types must match");
on_complete(*result, script, context)
}),
},
);
}
/// Returns a reference to the underlying, low level task pool, that could be used to for special
/// cases.
#[inline]
pub fn inner(&self) -> &Arc<TaskPool> {
&self.task_pool
}
#[inline]
pub(crate) fn pop_plugin_task_handler(&mut self, id: Uuid) -> Option<PluginTaskHandler> {
self.plugin_task_handlers.remove(&id)
}
#[inline]
pub(crate) fn pop_node_task_handler(&mut self, id: Uuid) -> Option<NodeTaskHandler> {
self.node_task_handlers.remove(&id)
}
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![cfg(target_arch = "wasm32")]
use crate::core::wasm_bindgen::{self, prelude::*};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn error(msg: String);
type Error;
#[wasm_bindgen(constructor)]
fn new() -> Error;
#[wasm_bindgen(structural, method, getter)]
fn stack(error: &Error) -> String;
}
fn custom_panic_hook(info: &std::panic::PanicHookInfo) {
let mut msg = info.to_string();
msg.push_str("\n\nStack:\n\n");
let e = Error::new();
let stack = e.stack();
msg.push_str(&stack);
msg.push_str("\n\n");
error(msg);
}
#[inline]
pub(super) fn set_panic_hook() {
use std::sync::Once;
static SET_HOOK: Once = Once::new();
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(custom_panic_hook));
});
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! 3D/2D Game Engine.
//!
//! Tutorials can be found [here](https://fyrox-book.github.io/tutorials/tutorials.html)
#![doc(
html_logo_url = "https://fyrox.rs/assets/logos/logo.png",
html_favicon_url = "https://fyrox.rs/assets/logos/logo.png"
)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::from_over_into)]
#![allow(clippy::approx_constant)]
#![allow(clippy::doc_lazy_continuation)]
#![allow(clippy::mutable_key_type)]
#![allow(mismatched_lifetime_syntaxes)]
pub mod engine;
pub mod material;
pub mod plugin;
pub mod renderer;
pub mod resource;
pub mod scene;
pub mod script;
pub mod utils;
pub use crate::core::rand;
pub use fxhash;
pub use walkdir;
pub use winit::*;
#[doc(inline)]
pub use fyrox_core as core;
#[doc(inline)]
pub use fyrox_animation as generic_animation;
#[doc(inline)]
pub use fyrox_graph as graph;
#[doc(inline)]
pub use fyrox_resource as asset;
#[doc(inline)]
pub use fyrox_ui as gui;
#[doc(inline)]
pub use fyrox_autotile as autotile;
#[doc(inline)]
pub use fyrox_graphics as graphics;
#[doc(inline)]
pub use fyrox_graphics_gl as graphics_gl;
/// Defines a builder's `with_xxx` method.
#[macro_export]
macro_rules! define_with {
($(#[$attr:meta])* fn $name:ident($field:ident: $ty:ty)) => {
$(#[$attr])*
pub fn $name(mut self, value: $ty) -> Self {
self.$field = value;
self
}
};
}
#[cfg(test)]
mod test {
use crate::scene::base::{Base, BaseBuilder};
use fyrox_core::reflect::Reflect;
use fyrox_core::ImmutableString;
use fyrox_sound::source::Status;
use fyrox_ui::widget::{Widget, WidgetBuilder};
use fyrox_ui::UserInterface;
#[test]
fn test_assembly_names() {
let mut ui = UserInterface::new(Default::default());
let var = ImmutableString::new("Foobar");
let base = BaseBuilder::new().build_base();
let widget = WidgetBuilder::new().build(&ui.build_ctx());
let status = Status::Stopped;
assert_eq!(var.type_info_ref().assembly_name, "fyrox-core");
assert_eq!(base.type_info_ref().assembly_name, "fyrox-impl");
assert_eq!(widget.type_info_ref().assembly_name, "fyrox-ui");
assert_eq!(status.type_info_ref().assembly_name, "fyrox-sound");
assert_eq!(ImmutableString::type_info().assembly_name, "fyrox-core");
assert_eq!(Base::type_info().assembly_name, "fyrox-impl");
assert_eq!(Widget::type_info().assembly_name, "fyrox-ui");
assert_eq!(Status::type_info().assembly_name, "fyrox-sound");
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Material is a set of parameters for a shader. This module contains everything related to materials.
//!
//! See [Material struct docs](self::Material) for more info.
pub use fyrox_material::*;
+347
View File
@@ -0,0 +1,347 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Dynamic plugins with hot-reloading ability.
use crate::{
core::{
info,
notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher},
warn,
},
plugin::{DynamicPlugin, Plugin},
};
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
},
};
/// Dynamic plugin, that is loaded from a dynamic library. Usually it is used for hot reloading,
/// it is strongly advised not to use it in production builds, because it is slower than statically
/// linked plugins and it could be unsafe if different compiler versions are used.
pub struct DyLibHandle {
pub(super) plugin: Box<dyn Plugin>,
// Keep the library loaded.
// Must be last!
#[allow(dead_code)]
#[cfg(any(unix, windows))]
lib: libloading::Library,
}
#[cfg(any(unix, windows))]
type PluginEntryPoint = fn() -> Box<dyn Plugin>;
impl DyLibHandle {
/// Tries to load a plugin from a dynamic library (*.dll on Windows, *.so on Unix).
pub fn load<P>(#[allow(unused_variables)] path: P) -> Result<Self, String>
where
P: libloading::AsFilename,
{
#[cfg(any(unix, windows))]
unsafe {
let lib = libloading::Library::new(path).map_err(|e| e.to_string())?;
let entry = lib
.get::<PluginEntryPoint>("fyrox_plugin".as_bytes())
.map_err(|e| e.to_string())?;
Ok(Self {
plugin: entry(),
lib,
})
}
#[cfg(not(any(unix, windows)))]
{
panic!("Unsupported platform!")
}
}
/// Return a reference to the plugin interface of the dynamic plugin.
pub fn plugin(&self) -> &dyn Plugin {
&*self.plugin
}
/// Return a reference to the plugin interface of the dynamic plugin.
pub(crate) fn plugin_mut(&mut self) -> &mut dyn Plugin {
&mut *self.plugin
}
}
/// Implementation of DynamicPluginTrait that (re)loads Rust code from Rust dylib .
pub struct DyLibDynamicPlugin {
/// Dynamic plugin state.
state: PluginState,
/// Target path of the library of the plugin.
lib_path: PathBuf,
/// Path to the source file, that is emitted by the compiler. If hot reloading is enabled,
/// this library will be cloned to `lib_path` and loaded. This is needed, because usually
/// OS locks the library and it is not possible to overwrite it while it is loaded in a process.
source_lib_path: PathBuf,
/// Optional file system watcher, that is configured to watch the source library and re-load
/// the plugin if the source library has changed. If the watcher is `None`, then hot reloading
/// is disabled.
_watcher: Option<RecommendedWatcher>,
/// A flag, that tells the engine that the plugin needs to be reloaded. Usually the engine
/// will do that at the end of the update tick.
need_reload: Arc<AtomicBool>,
}
impl DyLibDynamicPlugin {
/// Tries to create a new dynamic plugin. This method attempts to load a dynamic library by the
/// given path and searches for `fyrox_plugin` function. This function is called to create a
/// plugin instance. This method will fail if there's no dynamic library at the given path or
/// the `fyrox_plugin` function is not found.
///
/// # Hot reloading
///
/// This method can enable hot reloading for the plugin, by setting `reload_when_changed` parameter
/// to `true`. When enabled, the engine will clone the library to implementation-defined path
/// and load it. It will setup file system watcher to receive changes from the OS and reload
/// the plugin.
pub fn new<P>(
path: P,
reload_when_changed: bool,
use_relative_paths: bool,
) -> Result<Self, String>
where
P: AsRef<Path> + 'static,
{
let path = path.as_ref();
info!(
"Trying to create a new dylib plugin at {} path...",
path.display()
);
let current_exe = std::env::current_exe().map_err(|e| e.to_string())?;
let source_lib_path = if use_relative_paths {
let exe_folder = current_exe
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default();
exe_folder.join(path)
} else {
path.to_path_buf()
};
let plugin = if reload_when_changed {
info!(
"Trying to set up file system watcher for {} dylib...",
source_lib_path.display()
);
// Make sure each process will its own copy of the module. This is needed to prevent
// issues when there are two or more running processes and a library of the plugin
// changes. If the library is present in one instance in both (or more) processes, then
// it is impossible to replace it on disk. To prevent this, we need to add a suffix with
// executable name.
let mut suffix = current_exe
.file_stem()
.map(|s| s.to_owned())
.unwrap_or_default();
suffix.push(".module");
let lib_path = source_lib_path.with_extension(suffix);
try_copy_library(&source_lib_path, &lib_path)?;
let need_reload = Arc::new(AtomicBool::new(false));
let need_reload_clone = need_reload.clone();
let source_lib_path_clone = source_lib_path.clone();
let mut watcher =
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
if let Ok(event) = event {
if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
need_reload_clone.store(true, atomic::Ordering::Relaxed);
warn!(
"Plugin {} was changed. Performing hot reloading...",
source_lib_path_clone.display()
)
}
}
})
.map_err(|e| e.to_string())?;
watcher
.watch(&source_lib_path, RecursiveMode::NonRecursive)
.map_err(|e| e.to_string())?;
info!("Watching for changes in plugin {source_lib_path:?}...");
DyLibDynamicPlugin {
state: PluginState::Loaded(DyLibHandle::load(lib_path.as_os_str())?),
lib_path,
source_lib_path: source_lib_path.clone(),
_watcher: Some(watcher),
need_reload,
}
} else {
DyLibDynamicPlugin {
state: PluginState::Loaded(DyLibHandle::load(source_lib_path.as_os_str())?),
lib_path: source_lib_path.clone(),
source_lib_path: source_lib_path.clone(),
_watcher: None,
need_reload: Default::default(),
}
};
Ok(plugin)
}
}
impl DynamicPlugin for DyLibDynamicPlugin {
fn as_loaded_ref(&self) -> &dyn Plugin {
&*self.state.as_loaded_ref().plugin
}
fn as_loaded_mut(&mut self) -> &mut dyn Plugin {
&mut *self.state.as_loaded_mut().plugin
}
fn is_reload_needed_now(&self) -> bool {
self.need_reload.load(atomic::Ordering::Relaxed)
}
fn display_name(&self) -> String {
format!("{:?}", self.source_lib_path)
}
fn is_loaded(&self) -> bool {
matches!(self.state, PluginState::Loaded { .. })
}
fn reload(
&mut self,
fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>,
) -> Result<(), String> {
// Unload the plugin.
let PluginState::Loaded(_) = &mut self.state else {
return Err("cannot unload non-loaded plugin".to_string());
};
self.state = PluginState::Unloaded;
info!(
"Plugin {:?} was unloaded successfully!",
self.source_lib_path
);
// Replace the module.
try_copy_library(&self.source_lib_path, &self.lib_path)?;
info!(
"{:?} plugin's module {} was successfully cloned to {}.",
self.source_lib_path,
self.source_lib_path.display(),
self.lib_path.display()
);
let mut dynamic = DyLibHandle::load(&self.lib_path)?;
fill_and_register(dynamic.plugin_mut())?;
self.state = PluginState::Loaded(dynamic);
self.need_reload.store(false, atomic::Ordering::Relaxed);
info!(
"Plugin {:?} was reloaded successfully!",
self.source_lib_path
);
Ok(())
}
}
/// Actual state of a dynamic plugin.
enum PluginState {
/// Unloaded plugin.
Unloaded,
/// Loaded plugin.
Loaded(DyLibHandle),
}
impl PluginState {
/// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
pub fn as_loaded_ref(&self) -> &DyLibHandle {
match self {
PluginState::Unloaded => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
PluginState::Loaded(dynamic) => dynamic,
}
}
/// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
pub fn as_loaded_mut(&mut self) -> &mut DyLibHandle {
match self {
PluginState::Unloaded => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
PluginState::Loaded(dynamic) => dynamic,
}
}
}
fn try_copy_library(source_lib_path: &Path, lib_path: &Path) -> Result<(), String> {
info!(
"Trying to copy {} dylib to {} path...",
source_lib_path.display(),
lib_path.display()
);
if let Err(err) = std::fs::copy(source_lib_path, lib_path) {
// The library could already be copied and loaded, thus cannot be replaced. For
// example - by the running editor, that also uses hot reloading. Check for matching
// content, and if does not match, pass the error further.
let mut src_lib_file = File::open(source_lib_path).map_err(|e| e.to_string())?;
let mut src_lib_file_content = Vec::new();
src_lib_file
.read_to_end(&mut src_lib_file_content)
.map_err(|e| e.to_string())?;
let mut lib_file = File::open(lib_path).map_err(|e| e.to_string())?;
let mut lib_file_content = Vec::new();
lib_file
.read_to_end(&mut lib_file_content)
.map_err(|e| e.to_string())?;
if src_lib_file_content != lib_file_content {
return Err(format!(
"Unable to clone the library {} to {}. It is required, because source \
library has {} size, but loaded has {} size and the content does not match. \
Exact reason: {:?}",
source_lib_path.display(),
lib_path.display(),
src_lib_file_content.len(),
lib_file_content.len(),
err
));
}
}
Ok(())
}
+196
View File
@@ -0,0 +1,196 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Contains all possible errors that may occur during script or plugin methods execution.
use crate::{
asset::state::LoadError,
core::{dyntype::DynTypeError, pool::PoolError, visitor::error::VisitError, warn},
graphics::error::FrameworkError,
scene::graph::GraphError,
};
use std::{
backtrace::Backtrace,
fmt::{Debug, Display, Formatter},
sync::atomic::{AtomicBool, Ordering},
};
static CAPTURE_BACKTRACE: AtomicBool = AtomicBool::new(false);
/// Enables or disables backtrace capture when an error occurs. Backtrace capture is an expensive
/// operation, so it is disabled by default.
pub fn enable_backtrace_capture(capture: bool) {
CAPTURE_BACKTRACE.store(capture, Ordering::Relaxed);
if capture {
warn!(
"Backtrace capture is enabled! This will negatively impact performance in \
case of error spam."
)
}
}
/// Returns `true` when the backtrace capture is one, `false` - otherwise.
pub fn is_capturing_backtrace() -> bool {
CAPTURE_BACKTRACE.load(Ordering::Relaxed)
}
/// All possible errors that may occur during script or plugin methods execution.
pub enum GameErrorKind {
/// A [`GraphError`] has occurred.
GraphError(GraphError),
/// A [`PoolError`] has occurred.
PoolError(PoolError),
/// An arbitrary, user-defined error has occurred.
UserError(UserError),
/// A [`VisitError`] has occurred.
VisitError(VisitError),
/// A [`LoadError`] has occurred.
ResourceLoadError(LoadError),
/// A [`DynTypeError`] has occurred.
DynTypeError(DynTypeError),
/// Arbitrary error message.
StringError(String),
/// A [`FrameworkError`] has occurred.
FrameworkError(FrameworkError),
}
/// An error that may occur during game code execution.
pub struct GameError {
/// The actual kind of the error.
pub kind: GameErrorKind,
/// Optional stack trace of the error. See [`enable_backtrace_capture`] for more info.
pub trace: Option<Backtrace>,
}
impl GameError {
/// Creates a new error from the specified kind.
pub fn new(kind: GameErrorKind) -> Self {
Self {
kind,
trace: if is_capturing_backtrace() {
Some(Backtrace::force_capture())
} else {
None
},
}
}
/// A shortcut `GameError::user(value)` for `GameError::UserError(Box::new(value))`.
pub fn user(value: impl std::error::Error + Send + 'static) -> Self {
Self::new(GameErrorKind::UserError(Box::new(value)))
}
/// A shortcut for [`GameErrorKind::StringError`]
pub fn str(value: impl AsRef<str>) -> Self {
Self::new(GameErrorKind::StringError(value.as_ref().to_string()))
}
}
impl From<GraphError> for GameError {
fn from(value: GraphError) -> Self {
Self::new(GameErrorKind::GraphError(value))
}
}
impl From<PoolError> for GameError {
fn from(value: PoolError) -> Self {
Self::new(GameErrorKind::PoolError(value))
}
}
impl From<UserError> for GameError {
fn from(value: UserError) -> Self {
Self::new(GameErrorKind::UserError(value))
}
}
impl From<LoadError> for GameError {
fn from(value: LoadError) -> Self {
Self::new(GameErrorKind::ResourceLoadError(value))
}
}
impl From<VisitError> for GameError {
fn from(value: VisitError) -> Self {
Self::new(GameErrorKind::VisitError(value))
}
}
impl From<DynTypeError> for GameError {
fn from(value: DynTypeError) -> Self {
Self::new(GameErrorKind::DynTypeError(value))
}
}
impl From<FrameworkError> for GameError {
fn from(value: FrameworkError) -> Self {
Self::new(GameErrorKind::FrameworkError(value))
}
}
impl std::error::Error for GameError {}
impl Display for GameError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.trace.as_ref() {
Some(trace) => {
write!(f, "{}\nBacktrace:\n{}", self.kind, trace)
}
None => {
write!(
f,
"{}\nBacktrace is unavailable, call `enable_backtrace_capture(true)` to \
enable backtrace capture. Keep in mind that it may be very slow!",
self.kind
)
}
}
}
}
impl Display for GameErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::GraphError(err) => Display::fmt(&err, f),
Self::PoolError(err) => Display::fmt(&err, f),
Self::UserError(err) => Display::fmt(&err, f),
Self::ResourceLoadError(err) => Display::fmt(&err, f),
Self::VisitError(err) => Display::fmt(&err, f),
Self::DynTypeError(err) => Display::fmt(&err, f),
Self::StringError(msg) => {
write!(f, "{msg}")
}
Self::FrameworkError(err) => Display::fmt(&err, f),
}
}
}
impl Debug for GameError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
/// An alias for [`Result`] that has `()` as `Ok` value, and [`GameError`] as error value.
pub type GameResult = Result<(), GameError>;
/// An arbitrary, user-defined, boxed error type.
pub type UserError = Box<dyn std::error::Error + Send>;
+706
View File
@@ -0,0 +1,706 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Everything related to plugins. See [`Plugin`] docs for more info.
#![warn(missing_docs)]
pub mod dylib;
pub mod error;
use crate::{
asset::{manager::ResourceManager, untyped::UntypedResource},
core::{
define_as_any_trait, dyntype::DynTypeConstructorContainer, log::Log, pool::Handle,
reflect::Reflect, variable::try_inherit_properties, visitor::error::VisitError,
visitor::Visit,
},
engine::{
input::InputState, task::TaskPoolHandler, ApplicationLoopController, GraphicsContext,
PerformanceStatistics, ScriptProcessor, SerializationContext,
},
event::Event,
graph::NodeMapping,
gui::{
constructor::WidgetConstructorContainer,
inspector::editors::PropertyEditorDefinitionContainer, message::UiMessage, UiContainer,
UserInterface,
},
plugin::error::{GameError, GameResult},
resource::model::Model,
scene::{graph::NodePool, navmesh, Scene, SceneContainer, SceneLoader},
};
use fyrox_core::err;
use std::path::{Path, PathBuf};
use std::{
any::TypeId,
ops::{Deref, DerefMut},
sync::Arc,
};
/// A wrapper for various plugin types.
pub enum PluginContainer {
/// Statically linked plugin. Such plugins are meant to be used in final builds, to maximize
/// performance of the game.
Static(Box<dyn Plugin>),
/// Dynamically linked plugin. Such plugins are meant to be used in development mode for rapid
/// prototyping.
Dynamic(Box<dyn DynamicPlugin>),
}
/// Abstraction over different kind of plugins that can be reloaded on the fly (whatever it mean).
/// The instance is polled by engine with `is_reload_needed_now()` time to time. if it returns true,
/// then engine serializes current plugin state, then calls `unload()` and then calls `load()`
pub trait DynamicPlugin {
/// returns human-redable short description of the plugin
fn display_name(&self) -> String;
/// engine polls is time to time to determine if it's time to reload plugin
fn is_reload_needed_now(&self) -> bool;
/// panics if not loaded
fn as_loaded_ref(&self) -> &dyn Plugin;
/// panics if not loaded
fn as_loaded_mut(&mut self) -> &mut dyn Plugin;
/// returns false if something bad happends during `reload`.
/// has no much use except prevention of error spamming
fn is_loaded(&self) -> bool;
/// called before saving state and detaching related objects
fn prepare_to_reload(&mut self) {}
/// called after plugin-related objects are detached
/// `fill_and_register` callback exposes plugin instance to engine to register constructors and restore the state
/// callback approach allows plugins to do some necessary actions right after plugin is registed
fn reload(
&mut self,
fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>,
) -> Result<(), String>;
}
impl Deref for PluginContainer {
type Target = dyn Plugin;
fn deref(&self) -> &Self::Target {
match self {
PluginContainer::Static(plugin) => &**plugin,
PluginContainer::Dynamic(plugin) => plugin.as_loaded_ref(),
}
}
}
impl DerefMut for PluginContainer {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
PluginContainer::Static(plugin) => &mut **plugin,
PluginContainer::Dynamic(plugin) => plugin.as_loaded_mut(),
}
}
}
/// Output data of a loader.
pub struct LoaderOutput<T> {
/// The object produced by the loader.
pub payload: T,
/// A path of the source file.
pub path: PathBuf,
/// A raw data from which the loader output was created from. Usually it is just a content of
/// the source file.
pub data: Vec<u8>,
}
/// Alias for `LoaderOutput<Scene>`;
pub type SceneLoaderOutput = LoaderOutput<Scene>;
/// Alias for `Result<SceneLoaderOutput, VisitError>`;
pub type SceneLoaderResult = Result<SceneLoaderOutput, VisitError>;
/// Alias for `LoaderOutput<UserInterface>`
pub type UiLoaderOutput = LoaderOutput<UserInterface>;
/// Alias for `Result<UiLoaderOutput, VisitError>`
pub type UiLoaderResult = Result<UiLoaderOutput, VisitError>;
/// Contains plugin environment for the registration stage.
pub struct PluginRegistrationContext<'a> {
/// A reference to serialization context of the engine. See [`SerializationContext`] for more
/// info.
pub serialization_context: &'a Arc<SerializationContext>,
/// A reference to serialization context of the engine. See [`WidgetConstructorContainer`] for more
/// info.
pub widget_constructors: &'a Arc<WidgetConstructorContainer>,
/// A container with constructors for dynamic types. See [`DynTypeConstructorContainer`] for more
/// info.
pub dyn_type_constructors: &'a Arc<DynTypeConstructorContainer>,
/// A reference to the resource manager instance of the engine. Could be used to register resource loaders.
pub resource_manager: &'a ResourceManager,
}
/// Contains plugin environment.
pub struct PluginContext<'a, 'b> {
/// A reference to scene container of the engine. You can add new scenes from [`Plugin`] methods
/// by using [`SceneContainer::add`].
pub scenes: &'a mut SceneContainer,
/// A reference to the resource manager, it can be used to load various resources and manage
/// them. See [`ResourceManager`] docs for more info.
pub resource_manager: &'a ResourceManager,
/// A reference to user interface container of the engine. The engine guarantees that there's
/// at least one user interface exists. Use `context.user_interfaces.first()/first_mut()` to
/// get a reference to it.
pub user_interfaces: &'a mut UiContainer,
/// A reference to the graphics_context, it contains a reference to the window and the current renderer.
/// It could be [`GraphicsContext::Uninitialized`] if your application is suspended (possible only on
/// Android; it is safe to call [`GraphicsContext::as_initialized_ref`] or [`GraphicsContext::as_initialized_mut`]
/// on every other platform).
pub graphics_context: &'a mut GraphicsContext,
/// The time (in seconds) that passed since last call of a method in which the context was
/// passed. It has fixed value that is defined by a caller (in most cases it is `Executor`).
pub dt: f32,
/// A reference to time accumulator, that holds remaining amount of time that should be used
/// to update a plugin. A caller splits `lag` into multiple sub-steps using `dt` and thus
/// stabilizes update rate. The main use of this variable, is to be able to reset `lag` when
/// you doing some heavy calculations in a your game loop (i.e. loading a new level) so the
/// engine won't try to "catch up" with all the time that was spent in heavy calculation.
pub lag: &'b mut f32,
/// A reference to serialization context of the engine. See [`SerializationContext`] for more
/// info.
pub serialization_context: &'a Arc<SerializationContext>,
/// A reference to serialization context of the engine. See [`WidgetConstructorContainer`] for more
/// info.
pub widget_constructors: &'a Arc<WidgetConstructorContainer>,
/// A container with constructors for dynamic types. See [`DynTypeConstructorContainer`] for more
/// info.
pub dyn_type_constructors: &'a Arc<DynTypeConstructorContainer>,
/// Performance statistics from the last frame.
pub performance_statistics: &'a PerformanceStatistics,
/// Amount of time (in seconds) that passed from creation of the engine. Keep in mind, that
/// this value is **not** guaranteed to match real time. A user can change delta time with
/// which the engine "ticks" and this delta time affects elapsed time.
pub elapsed_time: f32,
/// Script processor is used to run script methods in a strict order.
pub script_processor: &'a ScriptProcessor,
/// Special field that associates the main application event loop (not game loop) with OS-specific
/// windows. It also can be used to alternate control flow of the application. `None` if the
/// engine is running in headless mode.
pub loop_controller: ApplicationLoopController<'b>,
/// Task pool for asynchronous task management.
pub task_pool: &'a mut TaskPoolHandler,
/// A stored state of most common input events. It is used a "shortcut" in cases where event-based
/// approach is too verbose. It may be useful in simple scenarios where you just need to know
/// if a button (on keyboard, mouse) was pressed and do something.
///
/// **Important:** this structure does not track from which device the corresponding event has
/// come from, if you have more than one keyboard and/or mouse, use event-based approach instead!
pub input_state: &'a InputState,
}
impl<'a, 'b> PluginContext<'a, 'b> {
/// Spawns an asynchronous task that tries to load a user interface from the given path.
/// When the task is completed, the specified callback is called that can be used to
/// modify the UI. The loaded UI must be registered in the engine, otherwise it will be
/// discarded.
///
/// ## Example
///
/// ```rust
/// # use fyrox_impl::{
/// # core::{pool::Handle, reflect::prelude::*, visitor::prelude::*},
/// # event::Event,
/// # plugin::{error::GameResult, Plugin, PluginContext, PluginRegistrationContext},
/// # scene::Scene,
/// # };
/// # use std::str::FromStr;
///
/// #[derive(Default, Visit, PartialEq, Reflect, Debug)]
/// #[reflect(non_cloneable, type_uuid = "ce9c7ff9-f490-4274-89c9-0b53b9f80532")]
/// struct MyGame {}
///
/// impl Plugin for MyGame {
/// fn init(&mut self, _scene_path: Option<&str>, mut ctx: PluginContext) -> GameResult {
/// ctx.load_ui("data/my.ui", |result, game: &mut MyGame, mut ctx| {
/// // The loaded UI must be registered in the engine.
/// ctx.user_interfaces.add(result?.payload);
/// Ok(())
/// });
/// Ok(())
/// }
/// }
/// ```
pub fn load_ui<U, P, C>(&mut self, path: U, callback: C)
where
U: Into<PathBuf>,
P: Plugin,
for<'c, 'd> C:
FnOnce(UiLoaderResult, &mut P, &mut PluginContext<'c, 'd>) -> GameResult + 'static,
{
let path = path.into();
self.task_pool.spawn_plugin_task(
UserInterface::load_from_file(
path.clone(),
self.widget_constructors.clone(),
self.dyn_type_constructors.clone(),
self.resource_manager.clone(),
),
move |result, plugin, ctx| match result {
Ok((ui, data)) => callback(
Ok(LoaderOutput {
payload: ui,
data,
path,
}),
plugin,
ctx,
),
Err(e) => callback(Err(e), plugin, ctx),
},
);
}
/// Tries to load a game scene at the given path.
///
/// This method has a special flag `is_derived` which dictates how to load the scene:
///
/// - `false` - the scene is loaded as-is and returned to the caller. Use this option if you
/// don't want to use built-in "saved game" system. See the next option for details.
/// - `true`, then the requested scene is loaded and a new model resource is
/// registered in the resource manager that references the scene source file. Then the loaded
/// scene is _cloned_ and all its nodes links to their originals in the source file. Then this
/// cloned and processed scene ("derived") is returned. This process essentially links the
/// scene to its source file, so when the derived scene is saved to disk, it does not save all
/// its content, but only changes from the original scene. Derived scenes are used to create
/// saved games. Keep in mind, if you've created a derived scene and saved it, you must load
/// this saved game with `is_derived` set to `false`.
///
/// # Example
///
/// ```rust
/// # use fyrox_impl::{
/// # core::{pool::Handle, reflect::prelude::*, visitor::prelude::*},
/// # event::Event,
/// # plugin::{error::GameResult, SceneLoaderResult, Plugin, PluginContext, PluginRegistrationContext},
/// # scene::Scene,
/// # };
/// # use std::str::FromStr;
/// #
/// #[derive(Default, Visit, PartialEq, Reflect, Debug)]
/// #[reflect(non_cloneable, type_uuid = "65b2da10-3926-48e4-93b4-01193cdc11ca")]
/// struct MyGame {}
///
/// impl MyGame {
/// fn on_scene_loading_result(&mut self, result: SceneLoaderResult, ctx: &mut PluginContext) -> GameResult {
/// // Register the scene.
/// ctx.scenes.add(result?.payload);
/// Ok(())
/// }
/// }
///
/// impl Plugin for MyGame {
/// fn init(&mut self, scene_path: Option<&str>, mut ctx: PluginContext) -> GameResult {
/// ctx.load_scene(
/// scene_path.unwrap_or("data/scene.rgs"),
/// false, // See the docs for details.
/// |result, game: &mut MyGame, ctx| game.on_scene_loading_result(result, ctx),
/// );
/// Ok(())
/// }
/// }
/// ```
pub fn load_scene<U, P, C>(&mut self, path: U, is_derived: bool, callback: C)
where
U: Into<PathBuf>,
P: Plugin,
for<'c, 'd> C:
FnOnce(SceneLoaderResult, &mut P, &mut PluginContext<'c, 'd>) -> GameResult + 'static,
{
let path = path.into();
let serialization_context = self.serialization_context.clone();
let dyn_type_constructors = self.dyn_type_constructors.clone();
let resource_manager = self.resource_manager.clone();
let uuid = resource_manager.find::<Model>(&path).resource_uuid();
let io = resource_manager.resource_io();
self.task_pool.spawn_plugin_task(
{
let path = path.clone();
async move {
match SceneLoader::from_file(
path,
io.as_ref(),
serialization_context,
dyn_type_constructors,
resource_manager.clone(),
)
.await
{
Ok((loader, data)) => Ok((loader.finish().await, data)),
Err(e) => Err(e),
}
}
},
move |result, plugin, ctx| {
match result {
Ok((mut scene, data)) => {
if is_derived {
let model = ctx.resource_manager.find_uuid::<Model>(uuid);
// Create a resource, that will point to the scene we've loaded the
// scene from and force scene nodes to inherit data from them.
let data = Model {
mapping: NodeMapping::UseHandles,
// We have to create a full copy of the scene, because otherwise
// some methods (`Base::root_resource` in particular) won't work
// correctly.
scene: scene.clone_one_to_one().0,
};
model.header().state.commit_ok(data);
for (handle, node) in scene.graph.pair_iter_mut() {
node.set_inheritance_data(handle, model.clone());
}
// Reset modified flags in every inheritable property of the scene.
// Except nodes, they're inherited in a separate place.
(&mut scene as &mut dyn Reflect).apply_recursively_mut(
&mut |object| {
let type_id = (*object).type_id();
if type_id != TypeId::of::<NodePool>() {
if let Some(variable) = object.as_inheritable_variable_mut()
{
variable.reset_modified_flag();
}
}
},
&[
TypeId::of::<UntypedResource>(),
TypeId::of::<navmesh::Container>(),
],
)
} else {
// Take scene data from the source scene.
if let Some(source_asset) =
scene.graph[scene.graph.get_root()].root_resource()
{
let source_asset_ref = source_asset.data_ref();
let source_scene_ref = &source_asset_ref.scene;
Log::verify(try_inherit_properties(
&mut scene,
source_scene_ref,
&[
TypeId::of::<NodePool>(),
TypeId::of::<UntypedResource>(),
TypeId::of::<navmesh::Container>(),
],
));
}
}
callback(
Ok(LoaderOutput {
payload: scene,
path,
data,
}),
plugin,
ctx,
)
}
Err(error) => callback(Err(error), plugin, ctx),
}
},
);
}
/// Tries to load either a game scene (via [`Self::load_scene`] or a user interface [`Self::load_ui`].
/// This method tries to guess the actual type of the asset by comparing the extension in the
/// path. When a game scene or a UI is loaded, it is added to the respective engine container.
pub fn load_scene_or_ui<P: Plugin>(&mut self, path: impl AsRef<Path>) {
let path = path.as_ref();
let ext = path
.extension()
.map(|ext| ext.to_string_lossy().to_string())
.unwrap_or_default();
match ext.as_str() {
"rgs" => self.load_scene(path, false, |result, _: &mut P, ctx| {
ctx.scenes.add(result?.payload);
Ok(())
}),
"ui" => self.load_ui(path, |result, _: &mut P, ctx| {
ctx.user_interfaces.add(result?.payload);
Ok(())
}),
_ => err!("File {path:?} is not a game scene nor a user interface!"),
}
}
}
define_as_any_trait!(PluginAsAny => Plugin);
impl dyn Plugin {
/// Performs downcasting to a particular type.
pub fn cast<T: Plugin>(&self) -> Option<&T> {
PluginAsAny::as_any(self).downcast_ref::<T>()
}
/// Performs downcasting to a particular type.
pub fn cast_mut<T: Plugin>(&mut self) -> Option<&mut T> {
PluginAsAny::as_any_mut(self).downcast_mut::<T>()
}
}
/// Plugin is a convenient interface that allow you to extend engine's functionality.
///
/// # Example
///
/// ```rust
/// # use fyrox_impl::{
/// # core::{pool::Handle}, core::visitor::prelude::*, core::reflect::prelude::*,
/// # plugin::{Plugin, PluginContext, PluginRegistrationContext, error::GameResult},
/// # scene::Scene,
/// # event::Event
/// # };
/// # use std::str::FromStr;
///
/// #[derive(Default, Visit, PartialEq, Reflect, Debug)]
/// #[reflect(non_cloneable, type_uuid = "342b2ef7-210d-465f-bcae-f59aa3a96420")]
/// struct MyPlugin {}
///
/// impl Plugin for MyPlugin {
/// fn on_deinit(&mut self, context: PluginContext) -> GameResult {
/// // The method is called when the plugin is disabling.
/// // The implementation is optional.
/// Ok(())
/// }
///
/// fn update(&mut self, context: &mut PluginContext) -> GameResult {
/// // The method is called on every frame, it is guaranteed to have fixed update rate.
/// // The implementation is optional.
/// Ok(())
/// }
///
/// fn on_os_event(&mut self, event: &Event<()>, context: PluginContext) -> GameResult {
/// // The method is called when the main window receives an event from the OS.
/// Ok(())
/// }
/// }
/// ```
///
/// # Error Handling
///
/// Every plugin method returns [`GameResult`] (which is a simple wrapper over `Result<(), GameError>`),
/// this helps to reduce the amount of boilerplate code related to error handling. There are a number
/// of errors that can be automatically handled via `?` operator. All supported error types listed
/// in [`error::GameError`] enum.
///
/// The following code snippet shows the most common use cases for error handling:
///
/// ```rust
/// # use fyrox_impl::{
/// # core::{err, pool::Handle, reflect::prelude::*, visitor::prelude::*},
/// # event::Event,
/// # graph::SceneGraph,
/// # plugin::{error::GameResult, Plugin, PluginContext, PluginRegistrationContext},
/// # scene::{node::Node, Scene},
/// # };
/// # use std::str::FromStr;
/// #[derive(Default, Visit, PartialEq, Reflect, Debug)]
/// #[reflect(non_cloneable, type_uuid = "94801cca-3f50-4bef-9b6f-ee2edd9dcb2b")]
/// struct MyPlugin {
/// scene: Handle<Scene>,
/// player: Handle<Node>,
/// }
///
/// impl Plugin for MyPlugin {
/// fn update(&mut self, context: &mut PluginContext) -> GameResult {
/// // 1. This is the old approach.
/// match context.scenes.try_get(self.scene) {
/// Ok(scene) => match scene.graph.try_get(self.player) {
/// Ok(player) => {
/// println!("Player name is: {}", player.name());
/// }
/// Err(error) => {
/// err!("Unable to borrow the player. Reason: {error}")
/// }
/// },
/// Err(error) => {
/// err!("Unable to borrow the scene. Reason: {error}")
/// }
/// }
///
/// // 2. This is the same code as above, but with shortcuts for easier error handling.
/// // Message report is will be something like this:
/// // `An error occurred during update plugin method call. Reason: <error message>`.
/// let scene = context.scenes.try_get(self.scene)?;
/// let player = scene.graph.try_get(self.player)?;
/// println!("Player name is: {}", player.name());
///
/// Ok(())
/// }
/// }
/// ```
pub trait Plugin: PluginAsAny + Visit + Reflect {
/// The method is called when the plugin constructor was just registered in the engine. The main
/// use of this method is to register scripts and custom scene graph nodes in [`SerializationContext`].
fn register(
&self,
#[allow(unused_variables)] context: PluginRegistrationContext,
) -> GameResult {
Ok(())
}
/// This method is used to register property editors for your game types; to make them editable
/// in the editor.
fn register_property_editors(
&self,
#[allow(unused_variables)] editors: Arc<PropertyEditorDefinitionContainer>,
) {
}
/// This method is used to initialize your plugin.
fn init(
&mut self,
#[allow(unused_variables)] scene_path: Option<&str>,
#[allow(unused_variables)] context: PluginContext,
) -> GameResult {
Ok(())
}
/// This method is called when your plugin was re-loaded from a dynamic library. It could be used
/// to restore some runtime state, that cannot be serialized. This method is called **only for
/// dynamic plugins!** It is guaranteed to be called after all plugins were constructed, so the
/// cross-plugins interactions are possible.
fn on_loaded(&mut self, #[allow(unused_variables)] context: PluginContext) -> GameResult {
Ok(())
}
/// The method is called before plugin will be disabled. It should be used for clean up, or some
/// additional actions.
fn on_deinit(&mut self, #[allow(unused_variables)] context: PluginContext) -> GameResult {
Ok(())
}
/// Updates the plugin internals at fixed rate (see [`PluginContext::dt`] parameter for more
/// info).
fn update(&mut self, #[allow(unused_variables)] context: &mut PluginContext) -> GameResult {
Ok(())
}
/// called after all Plugin and Script updates
fn post_update(
&mut self,
#[allow(unused_variables)] context: &mut PluginContext,
) -> GameResult {
Ok(())
}
/// The method is called when the main window receives an event from the OS. The main use of
/// the method is to respond to some external events, for example an event from keyboard or
/// gamepad. See [`Event`] docs for more info.
fn on_os_event(
&mut self,
#[allow(unused_variables)] event: &Event<()>,
#[allow(unused_variables)] context: PluginContext,
) -> GameResult {
Ok(())
}
/// The method is called when a graphics context was successfully created. It could be useful
/// to catch the moment when it was just created and do something in response.
fn on_graphics_context_initialized(
&mut self,
#[allow(unused_variables)] context: PluginContext,
) -> GameResult {
Ok(())
}
/// The method is called before the actual frame rendering. It could be useful to render off-screen
/// data (render something to texture, that can be used later in the main frame).
fn before_rendering(
&mut self,
#[allow(unused_variables)] context: PluginContext,
) -> GameResult {
Ok(())
}
/// The method is called when the current graphics context was destroyed.
fn on_graphics_context_destroyed(
&mut self,
#[allow(unused_variables)] context: PluginContext,
) -> GameResult {
Ok(())
}
/// The method will be called when there is any message from a user interface (UI) instance
/// of the engine. Use `ui_handle` parameter to find out from which UI the message has come
/// from.
fn on_ui_message(
&mut self,
#[allow(unused_variables)] context: &mut PluginContext,
#[allow(unused_variables)] message: &UiMessage,
#[allow(unused_variables)] ui_handle: Handle<UserInterface>,
) -> GameResult {
Ok(())
}
/// This method is called when a game error has occurred, allowing you to perform some
/// specific action to react to it (for example - to show an error message UI in your game).
///
/// ## Important notes
///
/// This method is called at the end of the current frame, and before that, the engine collects
/// all the errors into a queue and then processes them one by one. This means that this method
/// won't be called immediately when an error was returned by any of your plugin or script methods,
/// but instead the processing will be delayed to the end of the frame.
///
/// The error passed by a reference here instead of by-value, because there could be multiple
/// plugins that can handle the error. This might seem counterintuitive, but remember that
/// [`GameError`] can occur during script execution, which is not a part of a plugin and its
/// methods executed separately, outside the plugin routines.
///
/// ## Error handling
///
/// This method should return `true` if the error was handled and no logging is needed, otherwise
/// it should return `false` and in this case, the error will be logged by the engine. When
/// `true` is returned by the plugin, the error won't be passed to any other plugins. By default,
/// this method returns `false`, which means that it does not handle any errors and the engine
/// will log the errors as usual.
fn on_game_error(
&mut self,
#[allow(unused_variables)] context: &mut PluginContext,
#[allow(unused_variables)] error: &GameError,
) -> bool {
false
}
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{algebra::Vector2, math::Rect, sstorage::ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
geometry_buffer::GpuGeometryBuffer,
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
make_viewport_matrix,
resources::RendererResources,
RenderPassStatistics,
},
};
pub struct GaussianBlur {
h_framebuffer: GpuFrameBuffer,
v_framebuffer: GpuFrameBuffer,
width: usize,
height: usize,
}
fn create_framebuffer(
server: &dyn GraphicsServer,
name: &str,
width: usize,
height: usize,
pixel_kind: PixelKind,
) -> Result<GpuFrameBuffer, FrameworkError> {
server.create_frame_buffer(
None,
vec![Attachment::color(
server.create_2d_render_target(name, pixel_kind, width, height)?,
)],
)
}
impl GaussianBlur {
pub fn new(
server: &dyn GraphicsServer,
width: usize,
height: usize,
pixel_kind: PixelKind,
) -> Result<Self, FrameworkError> {
Ok(Self {
h_framebuffer: create_framebuffer(server, "HBlur", width, height, pixel_kind)?,
v_framebuffer: create_framebuffer(server, "VBlur", width, height, pixel_kind)?,
width,
height,
})
}
fn h_blurred(&self) -> &GpuTexture {
&self.h_framebuffer.color_attachments()[0].texture
}
pub fn result(&self) -> &GpuTexture {
&self.v_framebuffer.color_attachments()[0].texture
}
pub(crate) fn render(
&self,
server: &dyn GraphicsServer,
quad: &GpuGeometryBuffer,
input: &GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("GaussianBlur");
let mut stats = RenderPassStatistics::default();
let viewport = Rect::new(0, 0, self.width as i32, self.height as i32);
let inv_size = Vector2::new(1.0 / self.width as f32, 1.0 / self.height as f32);
let wvp = make_viewport_matrix(viewport);
for (image, framebuffer, horizontal) in [
(input, &self.h_framebuffer, true),
(self.h_blurred(), &self.v_framebuffer, false),
] {
let properties = PropertyGroup::from([
property("worldViewProjection", &wvp),
property("pixelSize", &inv_size),
property("horizontal", &horizontal),
]);
let material = RenderMaterial::from([
binding("image", (image, &renderer_resources.nearest_clamp_sampler)),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.gaussian_blur.run_pass(
1,
&ImmutableString::new("Primary"),
framebuffer,
quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
}
Ok(stats)
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::renderer::QualitySettings;
use crate::{
core::{math::Rect, ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
bloom::blur::GaussianBlur,
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
make_viewport_matrix,
resources::RendererResources,
RenderPassStatistics,
},
};
mod blur;
pub struct BloomRenderer {
framebuffer: GpuFrameBuffer,
blur: GaussianBlur,
width: usize,
height: usize,
}
impl BloomRenderer {
pub fn new(
server: &dyn GraphicsServer,
width: usize,
height: usize,
) -> Result<Self, FrameworkError> {
Ok(Self {
blur: GaussianBlur::new(server, width, height, PixelKind::RGB10A2)?,
framebuffer: server.create_frame_buffer(
None,
vec![Attachment::color(server.create_2d_render_target(
"Bloom",
PixelKind::RGB10A2,
width,
height,
)?)],
)?,
width,
height,
})
}
fn glow_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[0].texture
}
pub fn result(&self) -> &GpuTexture {
self.blur.result()
}
pub(crate) fn render(
&self,
server: &dyn GraphicsServer,
hdr_scene_frame: &GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
settings: &QualitySettings,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("Bloom");
let mut stats = RenderPassStatistics::default();
let viewport = Rect::new(0, 0, self.width as i32, self.height as i32);
let wvp = make_viewport_matrix(viewport);
let properties = PropertyGroup::from([
property("worldViewProjection", &wvp),
property("threshold", &settings.hdr_settings.bloom_settings.threshold),
]);
let material = RenderMaterial::from([
binding(
"hdrSampler",
(hdr_scene_frame, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.bloom.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
stats += self.blur.render(
server,
&renderer_resources.quad,
self.glow_texture(),
uniform_buffer_cache,
renderer_resources,
)?;
Ok(stats)
}
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
graphics::{
buffer::BufferUsage, error::FrameworkError, geometry_buffer::GpuGeometryBuffer,
server::GraphicsServer,
},
renderer::{
cache::{TemporaryCache, TimeToLive},
framework::GeometryBufferExt,
},
scene::mesh::surface::{SurfaceData, SurfaceResource},
};
struct SurfaceRenderData {
buffer: GpuGeometryBuffer,
vertex_modifications_count: u64,
triangles_modifications_count: u64,
layout_hash: u64,
}
#[derive(Default)]
pub struct GeometryCache {
buffer: TemporaryCache<SurfaceRenderData>,
}
fn create_geometry_buffer(
data: &SurfaceData,
server: &dyn GraphicsServer,
) -> Result<SurfaceRenderData, FrameworkError> {
let geometry_buffer = GpuGeometryBuffer::from_surface_data(
// TODO: It might be worth to add more informative name using a combination of the name of
// the parent scene node, surface index.
"GeometryBuffer",
data,
BufferUsage::StaticDraw,
server,
)?;
Ok(SurfaceRenderData {
buffer: geometry_buffer,
vertex_modifications_count: data.vertex_buffer.modifications_count(),
triangles_modifications_count: data.geometry_buffer.modifications_count(),
layout_hash: data.vertex_buffer.layout_hash(),
})
}
impl GeometryCache {
pub fn get<'a>(
&'a mut self,
server: &dyn GraphicsServer,
data: &SurfaceResource,
time_to_live: TimeToLive,
) -> Result<&'a GpuGeometryBuffer, FrameworkError> {
let data = data.data_ref();
match self
.buffer
.get_entry_mut_or_insert_with(&data.cache_index, time_to_live, || {
create_geometry_buffer(&data, server)
}) {
Ok(entry) => {
// We also must check if buffer's layout changed, and if so - recreate the entire
// buffer.
if entry.layout_hash == data.vertex_buffer.layout_hash() {
if data.vertex_buffer.modifications_count() != entry.vertex_modifications_count
{
// Vertices has changed, upload the new content.
entry
.buffer
.set_buffer_data(0, data.vertex_buffer.raw_data());
entry.vertex_modifications_count = data.vertex_buffer.modifications_count();
}
if data.geometry_buffer.modifications_count()
!= entry.triangles_modifications_count
{
// Triangles has changed, upload the new content.
entry
.buffer
.set_triangles(data.geometry_buffer.triangles_ref());
entry.triangles_modifications_count =
data.geometry_buffer.modifications_count();
}
} else {
let SurfaceRenderData {
buffer,
vertex_modifications_count,
triangles_modifications_count,
layout_hash,
} = create_geometry_buffer(&data, server)?;
entry.buffer = buffer;
entry.vertex_modifications_count = vertex_modifications_count;
entry.triangles_modifications_count = triangles_modifications_count;
entry.layout_hash = layout_hash;
}
Ok(&entry.buffer)
}
Err(err) => Err(err),
}
}
pub fn update(&mut self, dt: f32) {
self.buffer.update(dt);
}
pub fn clear(&mut self) {
self.buffer.clear();
}
pub fn alive_count(&self) -> usize {
self.buffer.alive_count()
}
}
+282
View File
@@ -0,0 +1,282 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(missing_docs)] // TODO
use crate::{
asset::entry::DEFAULT_RESOURCE_LIFETIME,
core::sparse::{AtomicIndex, SparseBuffer},
scene::mesh::{
buffer::{BytesStorage, TriangleBuffer, VertexAttributeDescriptor, VertexBuffer},
surface::{SurfaceData, SurfaceResource},
},
};
use fxhash::FxHashMap;
use fyrox_resource::untyped::ResourceKind;
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use uuid::Uuid;
pub mod geometry;
pub mod shader;
pub mod texture;
pub mod uniform;
#[derive(Copy, Clone, PartialEq)]
pub struct TimeToLive(pub f32);
impl Default for TimeToLive {
fn default() -> Self {
Self(DEFAULT_RESOURCE_LIFETIME)
}
}
impl Deref for TimeToLive {
type Target = f32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TimeToLive {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub struct CacheEntry<T> {
pub value: T,
pub time_to_live: TimeToLive,
pub self_index: Arc<AtomicIndex>,
}
impl<T> Drop for CacheEntry<T> {
fn drop(&mut self) {
// Reset self index to unassigned. This is needed, because there could be the following
// situation:
// 1) Cache entry was removed
// 2) Its index was stored somewhere else.
// 3) The index can then be used to access some entry on the index, but the cache cannot
// guarantee, that the data of the entry is the same.
self.self_index.set(AtomicIndex::UNASSIGNED_INDEX)
}
}
impl<T> Deref for CacheEntry<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for CacheEntry<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
pub struct TemporaryCache<T> {
pub buffer: SparseBuffer<CacheEntry<T>>,
}
impl<T> Default for TemporaryCache<T> {
fn default() -> Self {
Self {
buffer: Default::default(),
}
}
}
impl<T> TemporaryCache<T> {
pub fn spawn(
&mut self,
value: T,
self_index: Arc<AtomicIndex>,
time_to_live: TimeToLive,
) -> AtomicIndex {
let index = self.buffer.spawn(CacheEntry {
value,
time_to_live,
self_index,
});
self.buffer
.get_mut(&index)
.unwrap()
.self_index
.set(index.get());
index
}
pub fn get_mut(&mut self, index: &AtomicIndex) -> Option<&mut CacheEntry<T>> {
if let Some(entry) = self.buffer.get_mut(index) {
entry.time_to_live = TimeToLive::default();
Some(entry)
} else {
None
}
}
pub fn get_entry_mut_or_insert_with<F, E>(
&mut self,
index: &Arc<AtomicIndex>,
time_to_live: TimeToLive,
func: F,
) -> Result<&mut CacheEntry<T>, E>
where
F: FnOnce() -> Result<T, E>,
{
if let Some(entry) = self.buffer.get_mut(index) {
entry.time_to_live = time_to_live;
Ok(self.buffer.get_mut(index).unwrap())
} else {
let value = func()?;
let index = self.buffer.spawn(CacheEntry {
value,
time_to_live,
self_index: index.clone(),
});
let entry = self.buffer.get_mut(&index).unwrap();
entry.self_index.set(index.get());
Ok(entry)
}
}
pub fn get_mut_or_insert_with<F, E>(
&mut self,
index: &Arc<AtomicIndex>,
time_to_live: TimeToLive,
func: F,
) -> Result<&mut T, E>
where
F: FnOnce() -> Result<T, E>,
{
self.get_entry_mut_or_insert_with(index, time_to_live, func)
.map(|entry| &mut entry.value)
}
pub fn get_or_insert_with<F, E>(
&mut self,
index: &Arc<AtomicIndex>,
time_to_live: TimeToLive,
func: F,
) -> Result<&T, E>
where
F: FnOnce() -> Result<T, E>,
{
self.get_entry_mut_or_insert_with(index, time_to_live, func)
.map(|entry| &entry.value)
}
pub fn update(&mut self, dt: f32) {
for entry in self.buffer.iter_mut() {
*entry.time_to_live -= dt;
}
for i in 0..self.buffer.len() {
if let Some(entry) = self.buffer.get_raw(i) {
if *entry.time_to_live <= 0.0 {
self.buffer.free_raw(i);
}
}
}
}
pub fn clear(&mut self) {
self.buffer.clear();
}
pub fn alive_count(&self) -> usize {
self.buffer.filled()
}
pub fn remove(&mut self, index: &AtomicIndex) {
self.buffer.free(index);
}
}
/// A cache for dynamic surfaces, the content of which changes every frame. The main purpose of this
/// cache is to keep associated GPU buffers alive a long as the surfaces in the cache and thus prevent
/// redundant resource reallocation on every frame. This is very important for dynamic drawing, such
/// as 2D sprites, tile maps, etc.
#[derive(Default)]
pub struct DynamicSurfaceCache {
cache: FxHashMap<u64, SurfaceResource>,
}
impl DynamicSurfaceCache {
/// Creates a new empty cache.
pub fn new() -> Self {
Self::default()
}
/// Tries to get an existing surface from the cache using its unique id or creates a new one and
/// returns it.
pub fn get_or_create(
&mut self,
unique_id: u64,
layout: &[VertexAttributeDescriptor],
) -> SurfaceResource {
if let Some(surface) = self.cache.get_mut(&unique_id) {
surface.clone()
} else {
let default_capacity = 4096;
// Initialize empty vertex buffer.
let vertex_buffer = VertexBuffer::new_with_layout(
layout,
0,
BytesStorage::with_capacity(default_capacity),
)
.unwrap();
// Initialize empty triangle buffer.
let triangle_buffer = TriangleBuffer::new(Vec::with_capacity(default_capacity * 3));
let surface = SurfaceResource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
SurfaceData::new(vertex_buffer, triangle_buffer),
);
self.cache.insert(unique_id, surface.clone());
surface
}
}
pub fn alive_count(&self) -> usize {
self.cache.len()
}
/// Clears the surfaces in the cache, does **not** clear the cache itself.
pub fn clear(&mut self) {
for surface in self.cache.values_mut() {
let mut surface_data = surface.data_ref();
surface_data.vertex_buffer.modify().clear();
surface_data.geometry_buffer.modify().clear();
}
}
}
+422
View File
@@ -0,0 +1,422 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{arrayvec::ArrayVec, log::Log, math::Rect, sstorage::ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{DrawCallStatistics, GpuFrameBuffer, ResourceBindGroup, ResourceBinding},
geometry_buffer::GpuGeometryBuffer,
gpu_program::{GpuProgram, ShaderResourceDefinition, ShaderResourceKind},
gpu_texture::GpuTexture,
sampler::GpuSampler,
server::GraphicsServer,
uniform::StaticUniformBuffer,
DrawParameters, ElementRange,
},
material::{
shader::{Shader, ShaderResource},
MaterialPropertyRef,
},
renderer::{
bundle,
cache::{uniform::UniformBufferCache, TemporaryCache},
},
};
use fxhash::FxHashMap;
use std::ops::Deref;
pub struct NamedValue<T> {
pub name: ImmutableString,
pub value: T,
}
impl<T> NamedValue<T> {
pub fn new(name: impl Into<ImmutableString>, value: T) -> Self {
Self {
name: name.into(),
value,
}
}
}
pub struct NamedValuesContainer<T, const N: usize> {
properties: [NamedValue<T>; N],
}
fn search<'a, T>(slice: &'a [NamedValue<T>], name: &ImmutableString) -> Option<&'a NamedValue<T>> {
slice
.binary_search_by(|prop| prop.name.cached_hash().cmp(&name.cached_hash()))
.ok()
.and_then(|idx| slice.get(idx))
}
impl<T, const N: usize> NamedValuesContainer<T, N> {
pub fn property_ref(&self, name: &ImmutableString) -> Option<&NamedValue<T>> {
search(&self.properties, name)
}
pub fn data_ref(&self) -> NamedValuesContainerRef<'_, T> {
NamedValuesContainerRef {
properties: &self.properties,
}
}
}
impl<T, const N: usize> From<[NamedValue<T>; N]> for NamedValuesContainer<T, N> {
fn from(mut value: [NamedValue<T>; N]) -> Self {
value.sort_unstable_by_key(|prop| prop.name.cached_hash());
Self { properties: value }
}
}
impl<T, const N: usize> Deref for NamedValuesContainer<T, N> {
type Target = [NamedValue<T>];
fn deref(&self) -> &Self::Target {
&self.properties
}
}
pub struct NamedValuesContainerRef<'a, T> {
properties: &'a [NamedValue<T>],
}
impl<T> NamedValuesContainerRef<'_, T> {
pub fn property_ref(&self, name: &ImmutableString) -> Option<&NamedValue<T>> {
search(self.properties, name)
}
}
pub struct PropertyGroup<'a, const N: usize> {
pub properties: NamedValuesContainer<MaterialPropertyRef<'a>, N>,
}
pub type NamedPropertyRef<'a> = NamedValue<MaterialPropertyRef<'a>>;
pub fn property<'a>(
name: impl Into<ImmutableString>,
value: impl Into<MaterialPropertyRef<'a>>,
) -> NamedPropertyRef<'a> {
NamedValue::new(name, value.into())
}
pub fn binding<'a, 'b>(
name: impl Into<ImmutableString>,
value: impl Into<GpuResourceBinding<'a, 'b>>,
) -> NamedValue<GpuResourceBinding<'a, 'b>> {
NamedValue::new(name, value.into())
}
impl<'a> From<(&'a GpuTexture, &'a GpuSampler)> for GpuResourceBinding<'a, '_> {
fn from(value: (&'a GpuTexture, &'a GpuSampler)) -> Self {
GpuResourceBinding::Texture {
texture: value.0,
sampler: value.1,
}
}
}
impl<'a, 'b, const N: usize> From<&'a PropertyGroup<'b, N>> for GpuResourceBinding<'a, 'b> {
fn from(value: &'a PropertyGroup<'b, N>) -> Self {
GpuResourceBinding::PropertyGroup {
properties: value.properties.data_ref(),
}
}
}
impl<'a, const N: usize> From<[NamedPropertyRef<'a>; N]> for PropertyGroup<'a, N> {
fn from(value: [NamedPropertyRef<'a>; N]) -> Self {
Self {
properties: value.into(),
}
}
}
impl<'a, const N: usize> Deref for PropertyGroup<'a, N> {
type Target = [NamedValue<MaterialPropertyRef<'a>>];
fn deref(&self) -> &Self::Target {
&self.properties
}
}
pub enum GpuResourceBinding<'a, 'b> {
Texture {
texture: &'a GpuTexture,
sampler: &'a GpuSampler,
},
PropertyGroup {
properties: NamedValuesContainerRef<'a, MaterialPropertyRef<'b>>,
},
}
impl<'a, 'b> GpuResourceBinding<'a, 'b> {
pub fn texture(texture: &'a GpuTexture, sampler: &'a GpuSampler) -> Self {
Self::Texture { texture, sampler }
}
pub fn property_group<const N: usize>(properties: &'a PropertyGroup<'b, N>) -> Self {
Self::PropertyGroup {
properties: properties.properties.data_ref(),
}
}
}
pub struct RenderMaterial<'a, 'b, const N: usize> {
pub bindings: NamedValuesContainer<GpuResourceBinding<'a, 'b>, N>,
}
impl<'a, 'b, const N: usize> From<[NamedValue<GpuResourceBinding<'a, 'b>>; N]>
for RenderMaterial<'a, 'b, N>
{
fn from(value: [NamedValue<GpuResourceBinding<'a, 'b>>; N]) -> Self {
Self {
bindings: value.into(),
}
}
}
pub struct RenderPassData {
pub program: GpuProgram,
pub draw_params: DrawParameters,
}
pub struct RenderPassContainer {
pub resources: Vec<ShaderResourceDefinition>,
pub render_passes: FxHashMap<ImmutableString, RenderPassData>,
}
impl RenderPassContainer {
pub fn from_str(server: &dyn GraphicsServer, str: &str) -> Result<Self, FrameworkError> {
let shader = Shader::from_string(str).map_err(|e| FrameworkError::Custom(e.to_string()))?;
Self::new(server, &shader)
}
pub fn new(server: &dyn GraphicsServer, shader: &Shader) -> Result<Self, FrameworkError> {
let mut render_passes = FxHashMap::default();
for render_pass in shader.definition.passes.iter() {
let program_name = format!("{}_{}", shader.definition.name, render_pass.name);
match server.create_program(
&program_name,
render_pass.vertex_shader.0.clone(),
render_pass.vertex_shader_line,
render_pass.fragment_shader.0.clone(),
render_pass.fragment_shader_line,
&shader.definition.resources,
) {
Ok(gpu_program) => {
render_passes.insert(
ImmutableString::new(&render_pass.name),
RenderPassData {
program: gpu_program,
draw_params: render_pass.draw_parameters.clone(),
},
);
}
Err(e) => {
return Err(FrameworkError::Custom(format!(
"Failed to create {program_name} shader' GPU program. Reason: {e}"
)));
}
};
}
Ok(Self {
render_passes,
resources: shader.definition.resources.clone(),
})
}
pub fn get(
&self,
render_pass_name: &ImmutableString,
) -> Result<&RenderPassData, FrameworkError> {
self.render_passes.get(render_pass_name).ok_or_else(|| {
FrameworkError::Custom(format!("No render pass with name {render_pass_name}!"))
})
}
pub fn run_pass<const N: usize>(
&self,
instance_count: usize,
render_pass_name: &ImmutableString,
framebuffer: &GpuFrameBuffer,
geometry: &GpuGeometryBuffer,
viewport: Rect<i32>,
material: &RenderMaterial<'_, '_, N>,
uniform_buffer_cache: &mut UniformBufferCache,
element_range: ElementRange,
override_params: Option<&DrawParameters>,
) -> Result<DrawCallStatistics, FrameworkError> {
if instance_count == 0 {
return Ok(Default::default());
}
let render_pass = self.get(render_pass_name)?;
let mut resource_bindings = ArrayVec::<ResourceBinding, 32>::new();
for resource in self.resources.iter() {
// Ignore built-in groups.
if resource.is_built_in() {
continue;
}
match resource.kind {
ShaderResourceKind::Texture { .. } => {
if let Some((tex, sampler)) = material
.bindings
.property_ref(&resource.name)
.and_then(|p| {
if let GpuResourceBinding::Texture { texture, sampler } = p.value {
Some((texture, sampler))
} else {
None
}
})
{
resource_bindings.push(ResourceBinding::texture(
tex,
sampler,
resource.binding,
));
} else {
return Err(FrameworkError::Custom(format!(
"No texture bound to {} resource binding!",
resource.name
)));
}
}
ShaderResourceKind::PropertyGroup(ref shader_property_group) => {
let mut buf = StaticUniformBuffer::<16384>::new();
if let Some(material_property_group) = material
.bindings
.property_ref(&resource.name)
.and_then(|p| {
if let GpuResourceBinding::PropertyGroup { ref properties } = p.value {
Some(properties)
} else {
None
}
})
{
bundle::write_with_material(
shader_property_group,
material_property_group,
|c: &NamedValuesContainerRef<MaterialPropertyRef>, n| {
c.property_ref(n).map(|v| v.value)
},
&mut buf,
);
} else {
// No respective resource bound in the material, use shader defaults. This is very
// important, because some drivers will crash if uniform buffer has insufficient
// data.
bundle::write_shader_values(shader_property_group, &mut buf)
}
resource_bindings.push(ResourceBinding::buffer(
&uniform_buffer_cache.write(buf)?,
resource.binding,
Default::default(),
));
}
}
}
let resources = [ResourceBindGroup {
bindings: &resource_bindings,
}];
if instance_count == 1 {
framebuffer.draw(
geometry,
viewport,
&render_pass.program,
override_params.unwrap_or(&render_pass.draw_params),
&resources,
element_range,
)
} else {
framebuffer.draw_instances(
instance_count,
geometry,
viewport,
&render_pass.program,
override_params.unwrap_or(&render_pass.draw_params),
&resources,
element_range,
)
}
}
}
#[derive(Default)]
pub struct ShaderCache {
pub(super) cache: TemporaryCache<RenderPassContainer>,
}
impl ShaderCache {
pub fn remove(&mut self, shader: &ShaderResource) {
let mut state = shader.state();
if let Some(shader_state) = state.data() {
self.cache.remove(&shader_state.cache_index);
}
}
pub fn get(
&mut self,
server: &dyn GraphicsServer,
shader: &ShaderResource,
) -> Option<&RenderPassContainer> {
let mut shader_state = shader.state();
if let Some(shader_state) = shader_state.data() {
match self.cache.get_or_insert_with(
&shader_state.cache_index,
Default::default(),
|| RenderPassContainer::new(server, shader_state),
) {
Ok(shader_set) => Some(shader_set),
Err(error) => {
Log::err(format!("{error}"));
None
}
}
} else {
None
}
}
pub fn update(&mut self, dt: f32) {
self.cache.update(dt)
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn alive_count(&self) -> usize {
self.cache.alive_count()
}
}
+313
View File
@@ -0,0 +1,313 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::err_once,
core::log::{Log, MessageKind},
graphics::{
error::FrameworkError,
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
sampler::{
GpuSampler, GpuSamplerDescriptor, MagnificationFilter, MinificationFilter, WrapMode,
},
server::GraphicsServer,
},
renderer::cache::{TemporaryCache, TimeToLive},
resource::texture::{Texture, TextureResource},
};
use fyrox_texture::{
TextureKind, TextureMagnificationFilter, TextureMinificationFilter, TexturePixelKind,
TextureWrapMode,
};
use std::{borrow::Cow, time::Duration};
use uuid::Uuid;
#[derive(Clone)]
pub struct TextureRenderData {
pub gpu_texture: GpuTexture,
pub gpu_sampler: GpuSampler,
modifications_counter: u64,
sampler_modifications_counter: u64,
}
#[derive(Default)]
pub struct TextureCache {
cache: TemporaryCache<TextureRenderData>,
}
fn convert_texture_kind(v: TextureKind) -> GpuTextureKind {
match v {
TextureKind::Line { length } => GpuTextureKind::Line {
length: length as usize,
},
TextureKind::Rectangle { width, height } => GpuTextureKind::Rectangle {
width: width as usize,
height: height as usize,
},
TextureKind::Cube { size } => GpuTextureKind::Cube {
size: size as usize,
},
TextureKind::Volume {
width,
height,
depth,
} => GpuTextureKind::Volume {
width: width as usize,
height: height as usize,
depth: depth as usize,
},
}
}
pub fn convert_pixel_kind(texture_kind: TexturePixelKind) -> PixelKind {
match texture_kind {
TexturePixelKind::R8 => PixelKind::R8,
TexturePixelKind::RGB8 => PixelKind::RGB8,
TexturePixelKind::RGBA8 => PixelKind::RGBA8,
TexturePixelKind::RG8 => PixelKind::RG8,
TexturePixelKind::R16 => PixelKind::R16,
TexturePixelKind::RG16 => PixelKind::RG16,
TexturePixelKind::BGR8 => PixelKind::BGR8,
TexturePixelKind::BGRA8 => PixelKind::BGRA8,
TexturePixelKind::RGB16 => PixelKind::RGB16,
TexturePixelKind::RGBA16 => PixelKind::RGBA16,
TexturePixelKind::RGB16F => PixelKind::RGB16F,
TexturePixelKind::DXT1RGB => PixelKind::DXT1RGB,
TexturePixelKind::DXT1RGBA => PixelKind::DXT1RGBA,
TexturePixelKind::DXT3RGBA => PixelKind::DXT3RGBA,
TexturePixelKind::DXT5RGBA => PixelKind::DXT5RGBA,
TexturePixelKind::R8RGTC => PixelKind::R8RGTC,
TexturePixelKind::RG8RGTC => PixelKind::RG8RGTC,
TexturePixelKind::RGB32F => PixelKind::RGB32F,
TexturePixelKind::RGBA32F => PixelKind::RGBA32F,
TexturePixelKind::Luminance8 => PixelKind::L8,
TexturePixelKind::LuminanceAlpha8 => PixelKind::LA8,
TexturePixelKind::Luminance16 => PixelKind::L16,
TexturePixelKind::LuminanceAlpha16 => PixelKind::LA16,
TexturePixelKind::R32F => PixelKind::R32F,
TexturePixelKind::R16F => PixelKind::R16F,
TexturePixelKind::SRGBA8 => PixelKind::SRGBA8,
TexturePixelKind::SRGB8 => PixelKind::SRGB8,
}
}
pub fn convert_magnification_filter(v: TextureMagnificationFilter) -> MagnificationFilter {
match v {
TextureMagnificationFilter::Nearest => MagnificationFilter::Nearest,
TextureMagnificationFilter::Linear => MagnificationFilter::Linear,
}
}
pub fn convert_minification_filter(v: TextureMinificationFilter) -> MinificationFilter {
match v {
TextureMinificationFilter::Nearest => MinificationFilter::Nearest,
TextureMinificationFilter::NearestMipMapNearest => MinificationFilter::NearestMipMapNearest,
TextureMinificationFilter::NearestMipMapLinear => MinificationFilter::NearestMipMapLinear,
TextureMinificationFilter::Linear => MinificationFilter::Linear,
TextureMinificationFilter::LinearMipMapNearest => MinificationFilter::LinearMipMapNearest,
TextureMinificationFilter::LinearMipMapLinear => MinificationFilter::LinearMipMapLinear,
}
}
pub fn convert_wrap_mode(v: TextureWrapMode) -> WrapMode {
match v {
TextureWrapMode::Repeat => WrapMode::Repeat,
TextureWrapMode::ClampToEdge => WrapMode::ClampToEdge,
TextureWrapMode::ClampToBorder => WrapMode::ClampToBorder,
TextureWrapMode::MirroredRepeat => WrapMode::MirroredRepeat,
TextureWrapMode::MirrorClampToEdge => WrapMode::MirrorClampToEdge,
}
}
fn create_sampler(
server: &dyn GraphicsServer,
texture: &Texture,
) -> Result<GpuSampler, FrameworkError> {
server.create_sampler(GpuSamplerDescriptor {
mag_filter: convert_magnification_filter(texture.magnification_filter()),
min_filter: convert_minification_filter(texture.minification_filter()),
s_wrap_mode: convert_wrap_mode(texture.s_wrap_mode()),
t_wrap_mode: convert_wrap_mode(texture.t_wrap_mode()),
r_wrap_mode: convert_wrap_mode(texture.r_wrap_mode()),
anisotropy: texture.anisotropy_level(),
min_lod: texture.min_lod(),
max_lod: texture.max_lod(),
lod_bias: texture.lod_bias(),
})
}
fn create_gpu_texture(
server: &dyn GraphicsServer,
resource_manager: &ResourceManager,
uuid: &Uuid,
texture: &Texture,
) -> Result<TextureRenderData, FrameworkError> {
let path = resource_manager
.try_get_state(Duration::from_millis(1))
.and_then(|state| state.uuid_to_resource_path(*uuid));
let name = path
.as_ref()
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed(""));
let gpu_texture = server.create_texture(GpuTextureDescriptor {
name: &name,
kind: convert_texture_kind(texture.kind()),
pixel_kind: convert_pixel_kind(texture.pixel_kind()),
mip_count: texture.mip_count() as usize,
data: Some(texture.data()),
base_level: texture.base_level(),
max_level: texture.max_level(),
})?;
Ok(TextureRenderData {
gpu_texture,
gpu_sampler: create_sampler(server, texture)?,
modifications_counter: texture.modifications_count(),
sampler_modifications_counter: texture.sampler_modifications_count(),
})
}
impl TextureCache {
/// Unconditionally uploads requested texture into GPU memory, previous GPU texture will be automatically
/// destroyed.
pub fn upload(
&mut self,
server: &dyn GraphicsServer,
resource_manager: &ResourceManager,
texture: &TextureResource,
) -> Result<(), FrameworkError> {
let uuid = texture.resource_uuid();
let texture = texture.state();
if let Some(texture) = texture.data_ref() {
self.cache.get_entry_mut_or_insert_with(
&texture.cache_index,
Default::default(),
|| create_gpu_texture(server, resource_manager, &uuid, texture),
)?;
Ok(())
} else {
Err(FrameworkError::Custom(
"Texture is not loaded yet!".to_string(),
))
}
}
pub fn get(
&mut self,
server: &dyn GraphicsServer,
resource_manager: &ResourceManager,
texture_resource: &TextureResource,
) -> Option<&TextureRenderData> {
let uuid = texture_resource.resource_uuid();
let texture_data_guard = texture_resource.state();
if let Some(texture) = texture_data_guard.data_ref() {
match self.cache.get_mut_or_insert_with(
&texture.cache_index,
Default::default(),
|| create_gpu_texture(server, resource_manager, &uuid, texture),
) {
Ok(entry) => {
// Check if some value has changed in resource.
// Data might change from last frame, so we have to check it and upload new if so.
let modifications_count = texture.modifications_count();
if entry.modifications_counter != modifications_count {
if let Err(e) = entry.gpu_texture.set_data(
convert_texture_kind(texture.kind()),
convert_pixel_kind(texture.pixel_kind()),
texture.mip_count() as usize,
Some(texture.data()),
) {
Log::writeln(
MessageKind::Error,
format!("Unable to upload new texture data to GPU. Reason: {e:?}"),
)
} else {
entry.modifications_counter = modifications_count;
}
}
if entry.sampler_modifications_counter != texture.sampler_modifications_count()
{
entry.gpu_sampler = create_sampler(server, texture).unwrap();
}
return Some(entry);
}
Err(e) => {
drop(texture_data_guard);
err_once!(
texture_resource.key() as usize,
"Failed to create GPU texture from texture. Reason: {:?}",
e,
);
}
}
}
None
}
pub fn update(&mut self, dt: f32) {
self.cache.update(dt)
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn unload(&mut self, texture: &TextureResource) {
if let Some(texture) = texture.state().data() {
self.cache.remove(&texture.cache_index);
}
}
pub fn alive_count(&self) -> usize {
self.cache.alive_count()
}
/// Tries to bind existing GPU texture with a texture resource. If there's no such binding, then
/// a new binding is created, otherwise - only the TTL is updated to keep the GPU texture alive
/// for a certain time period (see [`TimeToLive`]).
pub fn try_register(
&mut self,
server: &dyn GraphicsServer,
texture: &TextureResource,
gpu_texture: GpuTexture,
) -> Result<(), FrameworkError> {
let data = texture.data_ref();
let index = data.cache_index.clone();
let entry = self.cache.get_mut(&index);
if entry.is_none() {
self.cache.spawn(
TextureRenderData {
gpu_texture,
gpu_sampler: create_sampler(server, &data)?,
modifications_counter: data.modifications_count(),
sampler_modifications_counter: data.sampler_modifications_count(),
},
index,
TimeToLive::default(),
);
}
Ok(())
}
}
+248
View File
@@ -0,0 +1,248 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Uniform buffer cache could be considered as pool of uniform buffers of fixed size. See
//! [`UniformBufferCache`] for more info.
use crate::graphics::{
buffer::{BufferKind, BufferUsage, GpuBuffer, GpuBufferDescriptor},
error::FrameworkError,
framebuffer::{BufferDataUsage, ResourceBinding},
server::{GraphicsServer, SharedGraphicsServer},
uniform::{ByteStorage, DynamicUniformBuffer, UniformBuffer},
};
use fxhash::FxHashMap;
use std::cell::RefCell;
#[derive(Default)]
struct UniformBufferSet {
buffers: Vec<GpuBuffer>,
free: usize,
}
impl UniformBufferSet {
fn mark_unused(&mut self) {
self.free = 0;
}
fn get_or_create(
&mut self,
size: usize,
server: &dyn GraphicsServer,
) -> Result<GpuBuffer, FrameworkError> {
if self.free < self.buffers.len() {
let buffer = &self.buffers[self.free];
self.free += 1;
Ok(buffer.clone())
} else {
let buffer = server.create_buffer(GpuBufferDescriptor {
name: &format!("UniformBuffer{}", self.buffers.len()),
size,
kind: BufferKind::Uniform,
usage: BufferUsage::StreamCopy,
})?;
self.buffers.push(buffer);
self.free = self.buffers.len();
Ok(self.buffers.last().unwrap().clone())
}
}
}
/// Uniform buffer cache could be considered as pool of uniform buffers of fixed size, that can be
/// used to fetch free buffer for drawing. Uniform buffers usually have quite limited size
/// (guaranteed to be at least 16kb and on vast majority of GPUs the upper limit is 65kb) and they
/// are intended to be used as a storage for relatively small set of data that can fit into L1 cache
/// of a GPU for very fast access.
pub struct UniformBufferCache {
server: SharedGraphicsServer,
cache: RefCell<FxHashMap<usize, UniformBufferSet>>,
}
impl UniformBufferCache {
pub fn new(server: SharedGraphicsServer) -> Self {
Self {
server,
cache: Default::default(),
}
}
/// Reserves one of the existing uniform buffers of the given size. If there's no such free buffer,
/// this method creates a new one and reserves it for further use.
pub fn get_or_create(&self, size: usize) -> Result<GpuBuffer, FrameworkError> {
let mut cache = self.cache.borrow_mut();
let set = cache.entry(size).or_default();
set.get_or_create(size, &*self.server)
}
/// Fetches a suitable (or creates new one) GPU uniform buffer for the given CPU uniform buffer
/// and writes the data to it, returns a reference to the buffer.
pub fn write<T>(&self, uniform_buffer: UniformBuffer<T>) -> Result<GpuBuffer, FrameworkError>
where
T: ByteStorage,
{
let data = uniform_buffer.finish();
let buffer = self.get_or_create(data.bytes_count())?;
buffer.write_data(data.bytes())?;
Ok(buffer)
}
/// Marks all reserved buffers as unused. Must be called at least once per frame to prevent
/// uncontrollable growth of the cache.
pub fn mark_all_unused(&mut self) {
for set in self.cache.borrow_mut().values_mut() {
set.mark_unused();
}
}
/// Returns the total amount of allocated uniforms buffers.
pub fn alive_count(&self) -> usize {
let mut count = 0;
for (_, set) in self.cache.borrow().iter() {
count += set.buffers.len();
}
count
}
}
struct Page {
dynamic: DynamicUniformBuffer,
is_submitted: bool,
}
/// The position of a uniform within a [`UniformMemoryAllocator`].
#[derive(Clone, Copy, Debug)]
pub struct UniformBlockLocation {
/// Index of the buffer in the buffer list of [`UniformMemoryAllocator`].
pub page: usize,
/// The position of the uniform within the buffer.
pub offset: usize,
/// The size of the uniform.
pub size: usize,
}
pub struct UniformMemoryAllocator {
gpu_buffers: Vec<GpuBuffer>,
block_alignment: usize,
max_uniform_buffer_size: usize,
pages: Vec<Page>,
blocks: Vec<UniformBlockLocation>,
}
impl UniformMemoryAllocator {
pub fn new(max_uniform_buffer_size: usize, block_alignment: usize) -> Self {
Self {
gpu_buffers: Default::default(),
block_alignment,
max_uniform_buffer_size,
pages: Default::default(),
blocks: Default::default(),
}
}
pub fn clear(&mut self) {
for page in self.pages.iter_mut() {
page.dynamic.clear();
page.is_submitted = false;
}
self.blocks.clear();
}
pub fn allocate<T>(&mut self, buffer: UniformBuffer<T>) -> UniformBlockLocation
where
T: ByteStorage,
{
let data = buffer.finish();
assert!(data.bytes_count() > 0);
assert!(data.bytes_count() < self.max_uniform_buffer_size);
let page_index = match self.pages.iter().position(|page| {
let write_position = page
.dynamic
.next_write_aligned_position(self.block_alignment);
self.max_uniform_buffer_size - write_position >= data.bytes_count()
}) {
Some(page_index) => page_index,
None => {
let page_index = self.pages.len();
self.pages.push(Page {
dynamic: UniformBuffer::with_storage(Vec::with_capacity(
self.max_uniform_buffer_size,
)),
is_submitted: false,
});
page_index
}
};
let page = &mut self.pages[page_index];
page.is_submitted = false;
let offset = page
.dynamic
.write_bytes_with_alignment(data.bytes(), self.block_alignment);
let block = UniformBlockLocation {
page: page_index,
offset,
size: data.bytes_count(),
};
self.blocks.push(block);
block
}
pub fn upload(&mut self, server: &dyn GraphicsServer) -> Result<(), FrameworkError> {
if self.gpu_buffers.len() < self.pages.len() {
for _ in 0..(self.pages.len() - self.gpu_buffers.len()) {
let buffer = server.create_buffer(GpuBufferDescriptor {
name: &format!("UniformMemoryPage{}", self.gpu_buffers.len()),
size: self.max_uniform_buffer_size,
kind: BufferKind::Uniform,
usage: BufferUsage::StreamCopy,
})?;
self.gpu_buffers.push(buffer);
}
}
for (page, gpu_buffer) in self.pages.iter_mut().zip(self.gpu_buffers.iter()) {
if !page.is_submitted {
let bytes = page.dynamic.storage().bytes();
assert!(bytes.len() <= self.max_uniform_buffer_size);
gpu_buffer.write_data(bytes)?;
page.is_submitted = true;
}
}
Ok(())
}
pub fn block_to_binding(
&self,
block: UniformBlockLocation,
binding_point: usize,
) -> ResourceBinding {
ResourceBinding::Buffer {
buffer: self.gpu_buffers[block.page].clone(),
binding: binding_point,
data_usage: BufferDataUsage::UseSegment {
offset: block.offset,
size: block.size,
},
}
}
}
+214
View File
@@ -0,0 +1,214 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Contains various algorithms for image convolution, primarily for lighting.
use crate::{
core::{
algebra::{Matrix4, Point3},
color::Color,
math::Rect,
some_or_break, ImmutableString,
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
stats::RenderPassStatistics,
ElementRange,
},
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
resources::RendererResources,
utils::CubeMapFaceDescriptor,
},
};
pub struct EnvironmentMapSpecularConvolution {
framebuffer: GpuFrameBuffer,
mip_count: usize,
pub(crate) size: usize,
}
impl EnvironmentMapSpecularConvolution {
pub fn new(server: &dyn GraphicsServer, size: usize) -> Result<Self, FrameworkError> {
let mip_count = ((size as f32).log2().floor() + 1.0) as usize;
let cube_map = server.create_texture(GpuTextureDescriptor {
name: "EnvironmentMapSpecularConvolution",
kind: GpuTextureKind::Cube { size },
pixel_kind: PixelKind::RGB8,
mip_count,
data: None,
base_level: 0,
max_level: mip_count,
})?;
Ok(Self {
framebuffer: server.create_frame_buffer(None, vec![Attachment::color(cube_map)])?,
mip_count,
size,
})
}
pub fn cube_map(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[0].texture
}
pub fn render(
&self,
server: &dyn GraphicsServer,
environment_map: &GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("EnvironmentMapSpecularConvolution");
let mut stats = RenderPassStatistics::default();
let projection_matrix =
Matrix4::new_perspective(1.0, std::f32::consts::FRAC_PI_2, 0.0125, 32.0);
for mip in 0..self.mip_count {
let roughness = (mip as f32) / (self.mip_count - 1) as f32;
let size = some_or_break!(self.size.checked_shr(mip as u32));
let viewport = Rect::new(0, 0, size as i32, size as i32);
for face in CubeMapFaceDescriptor::cube_faces() {
self.framebuffer.set_cubemap_face(0, face.face, mip);
self.framebuffer
.clear(viewport, Some(Color::WHITE), None, None);
let view_matrix =
Matrix4::look_at_rh(&Default::default(), &Point3::from(face.look), &face.up);
let wvp = projection_matrix * view_matrix;
let properties = PropertyGroup::from([
property("worldViewProjection", &wvp),
property("roughness", &roughness),
]);
let material = RenderMaterial::from([
binding(
"environmentMap",
(environment_map, &renderer_resources.linear_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources
.shaders
.environment_map_specular_convolution
.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.cube,
viewport,
&material,
uniform_buffer_cache,
ElementRange::Full,
None,
)?;
}
}
Ok(stats)
}
}
pub struct EnvironmentMapIrradianceConvolution {
framebuffer: GpuFrameBuffer,
size: usize,
}
impl EnvironmentMapIrradianceConvolution {
pub fn new(server: &dyn GraphicsServer, size: usize) -> Result<Self, FrameworkError> {
let cube_map = server.create_texture(GpuTextureDescriptor {
name: "EnvironmentMapIrradianceConvolution",
kind: GpuTextureKind::Cube { size },
pixel_kind: PixelKind::RGB8,
mip_count: 1,
data: None,
base_level: 0,
max_level: 1,
})?;
Ok(Self {
framebuffer: server.create_frame_buffer(None, vec![Attachment::color(cube_map)])?,
size,
})
}
pub fn cube_map(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[0].texture
}
pub fn render(
&self,
server: &dyn GraphicsServer,
environment_map: &GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("EnvironmentMapIrradianceConvolution");
let mut stats = RenderPassStatistics::default();
let projection_matrix =
Matrix4::new_perspective(1.0, std::f32::consts::FRAC_PI_2, 0.0125, 32.0);
let viewport = Rect::new(0, 0, self.size as i32, self.size as i32);
for face in CubeMapFaceDescriptor::cube_faces() {
self.framebuffer.set_cubemap_face(0, face.face, 0);
self.framebuffer
.clear(viewport, Some(Color::WHITE), None, None);
let view_matrix =
Matrix4::look_at_rh(&Default::default(), &Point3::from(face.look), &face.up);
let wvp = projection_matrix * view_matrix;
let properties = PropertyGroup::from([property("worldViewProjection", &wvp)]);
let material = RenderMaterial::from([
binding(
"environmentMap",
(environment_map, &renderer_resources.linear_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources
.shaders
.environment_map_irradiance_convolution
.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.cube,
viewport,
&material,
uniform_buffer_cache,
ElementRange::Full,
None,
)?;
}
Ok(stats)
}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Debug renderer allows you to create debug geometry (wireframe) on the fly. As it said
//! in its name its purpose - output debug information. It can be used to render collision
//! shapes, contact information (normals, positions, etc.), paths build by navmesh and so
//! on. It contains implementations to draw most common shapes (line, box, oob, frustum, etc).
use crate::{
core::{
algebra::{Matrix4, Vector3},
color::Color,
math::Rect,
sstorage::ImmutableString,
},
graphics::{
buffer::BufferUsage,
error::FrameworkError,
framebuffer::GpuFrameBuffer,
geometry_buffer::{
AttributeDefinition, AttributeKind, ElementsDescriptor, GpuGeometryBuffer,
GpuGeometryBufferDescriptor, VertexBufferData, VertexBufferDescriptor,
},
server::GraphicsServer,
},
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
resources::RendererResources,
RenderPassStatistics,
},
scene::debug::Line,
};
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Pod, Zeroable, Clone)]
struct Vertex {
position: Vector3<f32>,
color: u32,
}
/// See module docs.
pub struct DebugRenderer {
geometry: GpuGeometryBuffer,
vertices: Vec<Vertex>,
line_indices: Vec<[u32; 2]>,
}
/// "Draws" a rectangle into a list of lines.
pub fn draw_rect(rect: &Rect<f32>, lines: &mut Vec<Line>, color: Color) {
for (a, b) in [
(rect.left_top_corner(), rect.right_top_corner()),
(rect.right_top_corner(), rect.right_bottom_corner()),
(rect.right_bottom_corner(), rect.left_bottom_corner()),
(rect.left_bottom_corner(), rect.left_top_corner()),
] {
lines.push(Line {
begin: a.to_homogeneous(),
end: b.to_homogeneous(),
color,
});
}
}
impl DebugRenderer {
pub(crate) fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
let desc = GpuGeometryBufferDescriptor {
name: "DebugGeometryBuffer",
elements: ElementsDescriptor::Lines(&[]),
buffers: &[VertexBufferDescriptor {
usage: BufferUsage::DynamicDraw,
attributes: &[
AttributeDefinition {
location: 0,
divisor: 0,
kind: AttributeKind::Float,
component_count: 3,
normalized: false,
},
AttributeDefinition {
location: 1,
kind: AttributeKind::UnsignedByte,
component_count: 4,
normalized: true,
divisor: 0,
},
],
data: VertexBufferData::new::<Vertex>(None),
}],
usage: BufferUsage::DynamicDraw,
};
Ok(Self {
geometry: server.create_geometry_buffer(desc)?,
vertices: Default::default(),
line_indices: Default::default(),
})
}
/// Uploads the new set of lines to GPU.
pub fn set_lines(&mut self, lines: &[Line]) {
self.vertices.clear();
self.line_indices.clear();
let mut i = 0;
for line in lines.iter() {
let color = line.color.into();
self.vertices.push(Vertex {
position: line.begin,
color,
});
self.vertices.push(Vertex {
position: line.end,
color,
});
self.line_indices.push([i, i + 1]);
i += 2;
}
self.geometry.set_buffer_data_of_type(0, &self.vertices);
self.geometry.set_lines(&self.line_indices);
}
pub(crate) fn render(
&mut self,
server: &dyn GraphicsServer,
uniform_buffer_cache: &mut UniformBufferCache,
viewport: Rect<i32>,
framebuffer: &GpuFrameBuffer,
view_projection: Matrix4<f32>,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("DebugRendering");
let mut statistics = RenderPassStatistics::default();
let properties = PropertyGroup::from([property("worldViewProjection", &view_projection)]);
let material = RenderMaterial::from([binding("properties", &properties)]);
statistics += renderer_resources.shaders.debug.run_pass(
1,
&ImmutableString::new("Primary"),
framebuffer,
&self.geometry,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
Ok(statistics)
}
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Rendering framework.
use crate::{
graphics::{
buffer::BufferUsage,
error::FrameworkError,
geometry_buffer::{
AttributeDefinition, AttributeKind, GpuGeometryBufferDescriptor, VertexBufferData,
VertexBufferDescriptor,
},
server::GraphicsServer,
},
scene::mesh::{buffer::VertexAttributeDataType, surface::SurfaceData},
};
use fyrox_graphics::geometry_buffer::{ElementsDescriptor, GpuGeometryBuffer};
/// Extension trait for [`GpuGeometryBuffer`].
pub trait GeometryBufferExt {
/// Creates [`GpuGeometryBuffer`] from [`SurfaceData`].
fn from_surface_data(
name: &str,
data: &SurfaceData,
usage: BufferUsage,
server: &dyn GraphicsServer,
) -> Result<GpuGeometryBuffer, FrameworkError>;
}
impl GeometryBufferExt for GpuGeometryBuffer {
fn from_surface_data(
name: &str,
data: &SurfaceData,
usage: BufferUsage,
server: &dyn GraphicsServer,
) -> Result<GpuGeometryBuffer, FrameworkError> {
let attributes = data
.vertex_buffer
.layout()
.iter()
.map(|a| AttributeDefinition {
location: a.shader_location as u32,
kind: match a.data_type {
VertexAttributeDataType::F32 => AttributeKind::Float,
VertexAttributeDataType::U32 => AttributeKind::UnsignedInt,
VertexAttributeDataType::U16 => AttributeKind::UnsignedShort,
VertexAttributeDataType::U8 => AttributeKind::UnsignedByte,
},
component_count: a.size as usize,
normalized: a.normalized,
divisor: a.divisor as u32,
})
.collect::<Vec<_>>();
let geometry_buffer_desc = GpuGeometryBufferDescriptor {
name,
buffers: &[VertexBufferDescriptor {
usage,
attributes: &attributes,
data: VertexBufferData {
element_size: data.vertex_buffer.vertex_size() as usize,
bytes: Some(data.vertex_buffer.raw_data()),
},
}],
usage,
elements: ElementsDescriptor::Triangles(data.geometry_buffer.triangles_ref()),
};
server.create_geometry_buffer(geometry_buffer_desc)
}
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{algebra::Vector2, math::Rect, sstorage::ImmutableString},
graphics::{error::FrameworkError, framebuffer::GpuFrameBuffer, gpu_texture::GpuTexture},
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
make_viewport_matrix,
resources::RendererResources,
RenderPassStatistics,
},
};
use fyrox_graphics::server::GraphicsServer;
#[derive(Default)]
pub struct FxaaRenderer {}
impl FxaaRenderer {
pub(crate) fn render(
&self,
server: &dyn GraphicsServer,
viewport: Rect<i32>,
frame_texture: &GpuTexture,
frame_buffer: &GpuFrameBuffer,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("FXAA");
let mut statistics = RenderPassStatistics::default();
let frame_matrix = make_viewport_matrix(viewport);
let inv_screen_size = Vector2::new(1.0 / viewport.w() as f32, 1.0 / viewport.h() as f32);
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("inverseScreenSize", &inv_screen_size),
]);
let material = RenderMaterial::from([
binding(
"screenTexture",
(frame_texture, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
statistics += renderer_resources.shaders.fxaa.run_pass(
1,
&ImmutableString::new("Primary"),
frame_buffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
Ok(statistics)
}
}
+349
View File
@@ -0,0 +1,349 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! GBuffer Layout:
//!
//! RT0: sRGBA8 - Diffuse color (xyz)
//! RT1: RGBA8 - Normal (xyz)
//! RT2: RGBA16F - Ambient light + emission (both in xyz)
//! RT3: RGBA8 - Metallic (x) + Roughness (y) + Ambient Occlusion (z)
//! RT4: R8UI - Decal mask (x)
//!
//! Every alpha channel is used for layer blending for terrains. This is inefficient, but for
//! now I don't know better solution.
use crate::{
core::{algebra::Vector2, color::Color, math::Rect, sstorage::ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
bundle::{BundleRenderContext, RenderDataBundleStorage, SurfaceInstanceData},
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial, ShaderCache},
uniform::{UniformBufferCache, UniformMemoryAllocator},
},
debug_renderer::DebugRenderer,
observer::Observer,
occlusion::OcclusionTester,
resources::RendererResources,
GeometryCache, QualitySettings, RenderPassStatistics, TextureCache,
},
scene::{decal::Decal, graph::Graph, mesh::RenderPath},
};
use fxhash::FxHashSet;
use fyrox_resource::manager::ResourceManager;
pub struct GBuffer {
framebuffer: GpuFrameBuffer,
decal_framebuffer: GpuFrameBuffer,
pub width: i32,
pub height: i32,
render_pass_name: ImmutableString,
occlusion_tester: OcclusionTester,
}
pub(crate) struct GBufferRenderContext<'a, 'b> {
pub server: &'a dyn GraphicsServer,
pub observer: &'b Observer,
pub geom_cache: &'a mut GeometryCache,
pub bundle_storage: &'a RenderDataBundleStorage,
pub texture_cache: &'a mut TextureCache,
pub shader_cache: &'a mut ShaderCache,
pub renderer_resources: &'a RendererResources,
pub quality_settings: &'a QualitySettings,
pub graph: &'b Graph,
pub uniform_buffer_cache: &'a mut UniformBufferCache,
pub uniform_memory_allocator: &'a mut UniformMemoryAllocator,
#[allow(dead_code)]
pub screen_space_debug_renderer: &'a mut DebugRenderer,
pub resource_manager: &'a ResourceManager,
}
impl GBuffer {
pub fn new(
server: &dyn GraphicsServer,
width: usize,
height: usize,
) -> Result<Self, FrameworkError> {
let diffuse_texture = server.create_2d_render_target(
"GBufferDiffuseTexture",
PixelKind::RGBA8,
width,
height,
)?;
let normal_texture = server.create_2d_render_target(
"GBufferNormalTexture",
PixelKind::RGB10A2,
width,
height,
)?;
let framebuffer = server.create_frame_buffer(
Some(Attachment::depth_stencil(server.create_2d_render_target(
"GBufferDepthStencilTexture",
PixelKind::D24S8,
width,
height,
)?)),
vec![
Attachment::color(diffuse_texture.clone()),
Attachment::color(normal_texture.clone()),
Attachment::color(server.create_2d_render_target(
"GBufferAmbientTexture",
PixelKind::RGB10A2,
width,
height,
)?),
Attachment::color(server.create_2d_render_target(
"GBufferMaterialTexture",
PixelKind::RGBA8,
width,
height,
)?),
Attachment::color(server.create_2d_render_target(
"GBufferDecalMaskTexture",
PixelKind::R8UI,
width,
height,
)?),
],
)?;
let decal_framebuffer = server.create_frame_buffer(
None,
vec![
Attachment::color(diffuse_texture),
Attachment::color(normal_texture),
],
)?;
Ok(Self {
framebuffer,
width: width as i32,
height: height as i32,
decal_framebuffer,
render_pass_name: ImmutableString::new("GBuffer"),
occlusion_tester: OcclusionTester::new(server, width, height, 16)?,
})
}
pub fn framebuffer(&self) -> &GpuFrameBuffer {
&self.framebuffer
}
pub fn depth(&self) -> &GpuTexture {
&self.framebuffer.depth_attachment().unwrap().texture
}
pub fn diffuse_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[0].texture
}
pub fn normal_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[1].texture
}
pub fn ambient_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[2].texture
}
pub fn material_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[3].texture
}
pub fn decal_mask_texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[4].texture
}
pub(crate) fn fill(
&mut self,
args: GBufferRenderContext,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = args.server.begin_scope("GBuffer");
let mut statistics = RenderPassStatistics::default();
let GBufferRenderContext {
server,
observer,
geom_cache,
bundle_storage,
texture_cache,
shader_cache,
quality_settings,
renderer_resources,
graph,
uniform_buffer_cache,
uniform_memory_allocator,
resource_manager,
..
} = args;
if quality_settings.use_occlusion_culling {
self.occlusion_tester.try_query_visibility_results(graph);
};
let viewport = Rect::new(0, 0, self.width, self.height);
self.framebuffer.clear(
viewport,
Some(Color::from_rgba(0, 0, 0, 0)),
Some(1.0),
Some(0),
);
let grid_cell = self
.occlusion_tester
.grid_cache
.cell(observer.position.translation);
let instance_filter = |instance: &SurfaceInstanceData| {
!quality_settings.use_occlusion_culling
|| grid_cell.is_none_or(|cell| cell.is_visible(instance.node_handle))
};
statistics += bundle_storage.render_to_frame_buffer(
server,
geom_cache,
shader_cache,
|bundle| bundle.render_path == RenderPath::Deferred,
instance_filter,
BundleRenderContext {
texture_cache,
render_pass_name: &self.render_pass_name,
frame_buffer: &self.framebuffer,
viewport,
uniform_memory_allocator,
resource_manager,
use_pom: quality_settings.use_parallax_mapping,
light_position: &Default::default(),
renderer_resources,
ambient_light: Color::WHITE, // TODO
scene_depth: None, // TODO. Add z-pre-pass.
},
)?;
if quality_settings.use_occlusion_culling {
let mut objects = FxHashSet::default();
for bundle in bundle_storage.bundles.iter() {
for instance in bundle.instances.iter() {
objects.insert(instance.node_handle);
}
}
self.occlusion_tester.try_run_visibility_test(
server,
graph,
None,
objects.iter(),
&self.framebuffer,
observer.position.translation,
observer.position.view_projection_matrix,
uniform_buffer_cache,
renderer_resources,
)?;
}
let inv_view_proj = observer
.position
.view_projection_matrix
.try_inverse()
.unwrap_or_default();
let depth = self.depth();
let decal_mask = self.decal_mask_texture();
let resolution = Vector2::new(self.width as f32, self.height as f32);
// Render decals after because we need to modify diffuse texture of G-Buffer and use depth texture
// for rendering. We'll render in the G-Buffer, but depth will be used from final frame, since
// decals do not modify depth (only diffuse and normal maps).
for decal in graph.linear_iter().filter_map(|n| n.cast::<Decal>()) {
let world_view_proj =
observer.position.view_projection_matrix * decal.global_transform();
let diffuse_texture = decal
.diffuse_texture()
.and_then(|t| {
texture_cache
.get(server, resource_manager, t)
.map(|t| (t.gpu_texture.clone(), t.gpu_sampler.clone()))
})
.unwrap_or((
renderer_resources.white_dummy.clone(),
renderer_resources.linear_clamp_sampler.clone(),
))
.clone();
let normal_texture = decal
.normal_texture()
.and_then(|t| {
texture_cache
.get(server, resource_manager, t)
.map(|t| (t.gpu_texture.clone(), t.gpu_sampler.clone()))
})
.unwrap_or((
renderer_resources.normal_dummy.clone(),
renderer_resources.linear_clamp_sampler.clone(),
))
.clone();
let inv_world_decal = decal.global_transform().try_inverse().unwrap_or_default();
let color = decal.color().srgb_to_linear_f32();
let layer_index = decal.layer() as u32;
let properties = PropertyGroup::from([
property("worldViewProjection", &world_view_proj),
property("invViewProj", &inv_view_proj),
property("invWorldDecal", &inv_world_decal),
property("resolution", &resolution),
property("color", &color),
property("layerIndex", &layer_index),
]);
let material = RenderMaterial::from([
binding(
"sceneDepth",
(depth, &renderer_resources.nearest_clamp_sampler),
),
binding("diffuseTexture", (&diffuse_texture.0, &diffuse_texture.1)),
binding("normalTexture", (&normal_texture.0, &normal_texture.1)),
binding(
"decalMask",
(decal_mask, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
statistics += renderer_resources.shaders.decal.run_pass(
1,
&ImmutableString::new("Primary"),
&self.decal_framebuffer,
&renderer_resources.cube,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
}
Ok(statistics)
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
graphics::{error::FrameworkError, gpu_texture::GpuTexture, server::GraphicsServer},
renderer::hdr::LumBuffer,
};
use std::cell::Cell;
pub struct AdaptationChain {
lum_framebuffers: [LumBuffer; 2],
swap: Cell<bool>,
}
pub struct AdaptationContext<'a> {
pub prev_lum: GpuTexture,
pub lum_buffer: &'a LumBuffer,
}
impl AdaptationChain {
pub fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
Ok(Self {
lum_framebuffers: [LumBuffer::new(server, 1)?, LumBuffer::new(server, 1)?],
swap: Cell::new(false),
})
}
pub fn begin(&self) -> AdaptationContext<'_> {
let out = if self.swap.get() {
AdaptationContext {
prev_lum: self.lum_framebuffers[0].framebuffer.color_attachments()[0]
.texture
.clone(),
lum_buffer: &self.lum_framebuffers[1],
}
} else {
AdaptationContext {
prev_lum: self.lum_framebuffers[1].framebuffer.color_attachments()[0]
.texture
.clone(),
lum_buffer: &self.lum_framebuffers[0],
}
};
self.swap.set(!self.swap.get());
out
}
pub fn avg_lum_texture(&self) -> &GpuTexture {
if self.swap.get() {
&self.lum_framebuffers[0].framebuffer.color_attachments()[0].texture
} else {
&self.lum_framebuffers[1].framebuffer.color_attachments()[0].texture
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub mod histogram_luminance_evaluator;
pub mod luminance_evaluator;
@@ -0,0 +1,187 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::renderer::hdr::luminance::luminance_evaluator::LuminanceEvaluator;
use std::fmt::Debug;
use std::ops::Range;
pub struct HistogramLuminanceEvaluator {
bin_count: usize,
luminance_value_range: Range<f32>,
sample_count: usize,
}
impl LuminanceEvaluator for HistogramLuminanceEvaluator {
fn average_luminance(self, data: &[f32]) -> f32 {
let mut histogram = LuminanceHistogram::new(self.bin_count, self.luminance_value_range);
for value in data {
histogram.push_value(*value);
}
histogram
.reduce_to_biggest_samples(self.sample_count)
.average_histogram_value()
}
}
impl Default for HistogramLuminanceEvaluator {
fn default() -> Self {
Self {
bin_count: 128,
luminance_value_range: 0.0f32..1.0f32,
sample_count: 5,
}
}
}
struct LuminanceHistogram {
bins: Vec<Vec<f32>>,
bin_width: f64,
}
impl LuminanceHistogram {
pub(crate) fn new(bin_count: usize, value_range: Range<f32>) -> Self {
let bin_width = (value_range.end as f64 - value_range.start as f64) / bin_count as f64;
let mut bins = Vec::with_capacity(bin_count + 1);
for _ in 0..bin_count + 1 {
bins.push(Vec::<f32>::new());
}
LuminanceHistogram { bins, bin_width }
}
pub(crate) fn push_value(&mut self, value: f32) {
let bin_index: usize = (value / self.bin_width as f32).floor() as usize;
self.bins[bin_index].push(value);
}
fn reduce_to_biggest_samples(self, sample_count: usize) -> Self {
let mut biggest_bins = Vec::<Vec<f32>>::with_capacity(sample_count);
let mut bins = self.bins;
for _ in 0..sample_count {
let mut index = 0;
for j in 0..bins.len() {
if bins[j].len() > bins[index].len() {
index = j;
}
}
let biggest_bin = bins.swap_remove(index);
biggest_bins.push(biggest_bin);
}
Self {
bins: biggest_bins,
bin_width: self.bin_width,
}
}
fn average_histogram_value(&self) -> f32 {
let value_count = self.bins.iter().map(|b| b.len()).sum::<usize>();
let sum = self
.bins
.iter()
.map(|bin| bin.iter().sum::<f32>())
.sum::<f32>();
sum / value_count as f32
}
}
impl Debug for LuminanceHistogram {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut lengths = Vec::with_capacity(self.bins.len());
for b in &self.bins {
lengths.push(b.len());
}
write!(f, "LuminanceHistogram {lengths:?}")
}
}
#[cfg(test)]
mod tests {
use crate::renderer::hdr::luminance::histogram_luminance_evaluator::{
HistogramLuminanceEvaluator, LuminanceHistogram,
};
use crate::renderer::hdr::luminance::luminance_evaluator::LuminanceEvaluator;
#[test]
fn test_integration_histogram_luminance_evaluator() {
let evaluator = HistogramLuminanceEvaluator::default();
let pixels = include!("test_luminance_data.in");
let average = evaluator.average_luminance(&pixels);
assert_eq!(average, 0.012671353);
}
#[test]
fn test_histogram_building() {
// Arrange
let pixels = include!("test_luminance_data.in");
// Act
let mut histogram = LuminanceHistogram::new(128, 0f32..1f32);
for p in pixels {
histogram.push_value(p);
}
// Assert
// Using bin length as checksum
let target_lengths = vec![
1515, 76, 891, 211, 104, 79, 59, 26, 23, 7, 36, 77, 75, 85, 68, 78, 61, 64, 43, 49, 22,
44, 27, 34, 26, 4, 55, 21, 26, 78, 47, 46, 19, 6, 1, 2, 0, 0, 1, 3, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let mut actual_lengths = Vec::with_capacity(target_lengths.len());
for b in histogram.bins {
actual_lengths.push(b.len());
}
assert_eq!(target_lengths, actual_lengths);
}
#[test]
fn test_reduction_to_sample_size() {
let pixels = vec![
5.0, 5.0, 5.0, 5.0, 5.0, 4.0, 4.0, 4.0, 4.0, 3.0, 3.0, 3.0, 2.0, 2.0, 1.0,
];
let mut histogram = LuminanceHistogram::new(5, 0f32..5f32);
for p in pixels {
histogram.push_value(p);
}
let h = histogram.reduce_to_biggest_samples(2);
assert_eq!(h.bins.len(), 2);
let target_bins = vec![vec![5.0, 5.0, 5.0, 5.0, 5.0], vec![4.0, 4.0, 4.0, 4.0]];
assert_eq!(h.bins, target_bins);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub trait LuminanceEvaluator {
fn average_luminance(self, data: &[f32]) -> f32;
}
File diff suppressed because it is too large Load Diff
+431
View File
@@ -0,0 +1,431 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Vector2},
color::Color,
math::Rect,
value_as_u8_slice, ImmutableString,
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, DrawCallStatistics, GpuFrameBuffer, ReadTarget},
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
},
renderer::{
bloom::BloomRenderer,
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
texture::TextureCache,
uniform::UniformBufferCache,
},
hdr::{adaptation::AdaptationChain, luminance::luminance_evaluator::LuminanceEvaluator},
make_viewport_matrix,
resources::RendererResources,
LuminanceCalculationMethod, QualitySettings, RenderPassStatistics,
},
scene::camera::{ColorGradingLut, Exposure},
};
mod adaptation;
mod luminance;
pub struct LumBuffer {
framebuffer: GpuFrameBuffer,
size: usize,
}
impl LumBuffer {
fn new(server: &dyn GraphicsServer, size: usize) -> Result<Self, FrameworkError> {
let texture =
server.create_2d_render_target("LuminanceTexture", PixelKind::R32F, size, size)?;
Ok(Self {
framebuffer: server.create_frame_buffer(None, vec![Attachment::color(texture)])?,
size,
})
}
fn clear(&self) {
self.framebuffer.clear(
Rect::new(0, 0, self.size as i32, self.size as i32),
Some(Color::BLACK),
None,
None,
);
}
fn matrix(&self) -> Matrix4<f32> {
make_viewport_matrix(Rect::new(0, 0, self.size as i32, self.size as i32))
}
fn texture(&self) -> &GpuTexture {
&self.framebuffer.color_attachments()[0].texture
}
}
pub struct HighDynamicRangeRenderer {
adaptation_chain: AdaptationChain,
downscale_chain: [LumBuffer; 6],
frame_luminance: LumBuffer,
stub_lut: GpuTexture,
/// Bloom contains only overly bright pixels that create light
/// bleeding effect (glow effect).
bloom_renderer: BloomRenderer,
}
pub struct HdrRendererArgs<'a> {
pub server: &'a dyn GraphicsServer,
pub hdr_scene_frame: &'a GpuTexture,
pub ldr_framebuffer: &'a GpuFrameBuffer,
pub viewport: Rect<i32>,
pub speed: f32,
pub exposure: Exposure,
pub color_grading_lut: Option<&'a ColorGradingLut>,
pub use_color_grading: bool,
pub texture_cache: &'a mut TextureCache,
pub uniform_buffer_cache: &'a mut UniformBufferCache,
pub renderer_resources: &'a RendererResources,
pub resource_manager: &'a ResourceManager,
pub settings: &'a QualitySettings,
}
impl HighDynamicRangeRenderer {
pub fn new(
width: usize,
height: usize,
server: &dyn GraphicsServer,
) -> Result<Self, FrameworkError> {
Ok(Self {
frame_luminance: LumBuffer::new(server, 64)?,
downscale_chain: [
LumBuffer::new(server, 32)?,
LumBuffer::new(server, 16)?,
LumBuffer::new(server, 8)?,
LumBuffer::new(server, 4)?,
LumBuffer::new(server, 2)?,
LumBuffer::new(server, 1)?,
],
adaptation_chain: AdaptationChain::new(server)?,
stub_lut: server.create_texture(GpuTextureDescriptor {
name: "StubHdrLut",
kind: GpuTextureKind::Volume {
width: 1,
height: 1,
depth: 1,
},
pixel_kind: PixelKind::RGB8,
data: Some(&[0, 0, 0]),
..Default::default()
})?,
bloom_renderer: BloomRenderer::new(server, width, height)?,
})
}
fn calculate_frame_luminance(
&self,
server: &dyn GraphicsServer,
scene_frame: &GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<DrawCallStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("CalculateFrameLuminance");
self.frame_luminance.clear();
let frame_matrix = self.frame_luminance.matrix();
let inv_size = Vector2::repeat(1.0 / self.frame_luminance.size as f32);
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("invSize", &inv_size),
]);
let material = RenderMaterial::from([
binding(
"frameSampler",
(scene_frame, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
renderer_resources.shaders.hdr_luminance.run_pass(
1,
&ImmutableString::new("Primary"),
&self.frame_luminance.framebuffer,
&renderer_resources.quad,
Rect::new(
0,
0,
self.frame_luminance.size as i32,
self.frame_luminance.size as i32,
),
&material,
uniform_buffer_cache,
Default::default(),
None,
)
}
fn calculate_avg_frame_luminance(
&self,
server: &dyn GraphicsServer,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
luminance_calculation_method: LuminanceCalculationMethod,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("CalculateAvgFrameLuminance");
let mut stats = RenderPassStatistics::default();
match luminance_calculation_method {
LuminanceCalculationMethod::Histogram => {
// TODO: Cloning memory from GPU to CPU is slow, but since the engine is limited
// by macOS's OpenGL 4.1 support and lack of compute shaders we'll build histogram
// manually on CPU anyway. Replace this with compute shaders whenever possible.
let pixels = self
.frame_luminance
.framebuffer
.read_pixels_of_type::<f32>(ReadTarget::Color(0))
.ok_or_else(|| {
FrameworkError::Custom("Unable to read luminance buffer!".to_string())
})?;
let evaluator =
luminance::histogram_luminance_evaluator::HistogramLuminanceEvaluator::default(
);
let avg_value = evaluator.average_luminance(&pixels);
self.downscale_chain.last().unwrap().texture().set_data(
GpuTextureKind::Rectangle {
width: 1,
height: 1,
},
PixelKind::R32F,
1,
Some(value_as_u8_slice(&avg_value)),
)?;
}
LuminanceCalculationMethod::DownSampling => {
let mut src = &self.frame_luminance;
for dest in self.downscale_chain.iter() {
let src_inv_size = Vector2::repeat(1.0 / src.size as f32);
let src_texture = src.texture();
let matrix = dest.matrix();
let properties = PropertyGroup::from([
property("worldViewProjection", &matrix),
property("invSize", &src_inv_size),
]);
let material = RenderMaterial::from([
binding(
"lumSampler",
(src_texture, &renderer_resources.linear_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.hdr_downscale.run_pass(
1,
&ImmutableString::new("Primary"),
&dest.framebuffer,
&renderer_resources.quad,
Rect::new(0, 0, dest.size as i32, dest.size as i32),
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
src = dest;
}
}
}
Ok(stats)
}
fn adaptation(
&self,
server: &dyn GraphicsServer,
speed: f32,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<DrawCallStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("Adaptation");
let ctx = self.adaptation_chain.begin();
let viewport = Rect::new(0, 0, ctx.lum_buffer.size as i32, ctx.lum_buffer.size as i32);
let matrix = ctx.lum_buffer.matrix();
let properties = PropertyGroup::from([
property("worldViewProjection", &matrix),
property("speed", &speed),
]);
let material = RenderMaterial::from([
binding(
"oldLumSampler",
(&ctx.prev_lum, &renderer_resources.nearest_clamp_sampler),
),
binding(
"newLumSampler",
(
self.downscale_chain.last().unwrap().texture(),
&renderer_resources.nearest_clamp_sampler,
),
),
binding("properties", &properties),
]);
renderer_resources.shaders.hdr_adaptation.run_pass(
1,
&ImmutableString::new("Primary"),
&ctx.lum_buffer.framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)
}
fn map_hdr_to_ldr(&self, args: HdrRendererArgs) -> Result<DrawCallStatistics, FrameworkError> {
let HdrRendererArgs {
server,
hdr_scene_frame,
ldr_framebuffer,
viewport,
exposure,
color_grading_lut,
use_color_grading,
texture_cache,
uniform_buffer_cache,
renderer_resources,
resource_manager,
settings,
..
} = args;
let _debug_scope = args.server.begin_scope("ToneMap");
let frame_matrix = make_viewport_matrix(viewport);
let color_grading_lut_tex = color_grading_lut
.and_then(|l| {
texture_cache
.get(server, resource_manager, l.lut_ref())
.map(|t| (&t.gpu_texture, &t.gpu_sampler))
})
.unwrap_or((&self.stub_lut, &renderer_resources.nearest_clamp_sampler));
let (is_auto, min_luminance, max_luminance, fixed_exposure) = match exposure {
Exposure::Auto {
min_luminance,
max_luminance,
} => (true, min_luminance, max_luminance, 0.0),
Exposure::Manual(fixed_exposure) => (false, 0.0, 0.0, fixed_exposure),
};
let bloom_texture = if settings.hdr_settings.bloom_settings.use_bloom {
self.bloom_renderer.result()
} else {
&renderer_resources.black_dummy
};
let color_grading_enabled = use_color_grading && color_grading_lut.is_some();
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("useColorGrading", &color_grading_enabled),
property("minLuminance", &min_luminance),
property("maxLuminance", &max_luminance),
property("autoExposure", &is_auto),
property("fixedExposure", &fixed_exposure),
]);
let material = RenderMaterial::from([
binding(
"hdrSampler",
(hdr_scene_frame, &renderer_resources.nearest_clamp_sampler),
),
binding(
"lumSampler",
(
self.adaptation_chain.avg_lum_texture(),
&renderer_resources.nearest_clamp_sampler,
),
),
binding(
"bloomSampler",
(bloom_texture, &renderer_resources.linear_clamp_sampler),
),
binding("colorMapSampler", color_grading_lut_tex),
binding("properties", &properties),
]);
renderer_resources.shaders.hdr_map.run_pass(
1,
&ImmutableString::new("Primary"),
ldr_framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)
}
pub fn render(&self, args: HdrRendererArgs) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = args.server.begin_scope("HDR");
let mut stats = RenderPassStatistics::default();
stats += self.calculate_frame_luminance(
args.server,
args.hdr_scene_frame,
args.uniform_buffer_cache,
args.renderer_resources,
)?;
stats += self.calculate_avg_frame_luminance(
args.server,
args.uniform_buffer_cache,
args.renderer_resources,
args.settings.hdr_settings.luminance_calculation_method,
)?;
if args.settings.hdr_settings.bloom_settings.use_bloom {
stats += self.bloom_renderer.render(
args.server,
args.hdr_scene_frame,
args.uniform_buffer_cache,
args.renderer_resources,
args.settings,
)?;
}
stats += self.adaptation(
args.server,
args.speed,
args.uniform_buffer_cache,
args.renderer_resources,
)?;
stats += self.map_hdr_to_ldr(args)?;
Ok(stats)
}
}
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{
algebra::{Isometry3, Matrix4, Point3, Translation, Vector3},
math::Rect,
sstorage::ImmutableString,
},
graphics::{
buffer::BufferUsage, error::FrameworkError, framebuffer::GpuFrameBuffer,
geometry_buffer::GpuGeometryBuffer, server::GraphicsServer,
},
renderer::{
bundle::{LightSource, LightSourceKind},
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
framework::GeometryBufferExt,
gbuffer::GBuffer,
make_viewport_matrix,
resources::RendererResources,
RenderPassStatistics,
},
scene::{graph::Graph, mesh::surface::SurfaceData},
};
pub struct LightVolumeRenderer {
cone: GpuGeometryBuffer,
sphere: GpuGeometryBuffer,
}
impl LightVolumeRenderer {
pub fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
Ok(Self {
cone: GpuGeometryBuffer::from_surface_data(
"Cone",
&SurfaceData::make_cone(
16,
1.0,
1.0,
&Matrix4::new_translation(&Vector3::new(0.0, -1.0, 0.0)),
),
BufferUsage::StaticDraw,
server,
)?,
sphere: GpuGeometryBuffer::from_surface_data(
"Sphere",
&SurfaceData::make_sphere(8, 8, 1.0, &Matrix4::identity()),
BufferUsage::StaticDraw,
server,
)?,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_volume(
&mut self,
server: &dyn GraphicsServer,
light: &LightSource,
gbuffer: &GBuffer,
view: Matrix4<f32>,
inv_proj: Matrix4<f32>,
view_proj: Matrix4<f32>,
viewport: Rect<i32>,
graph: &Graph,
frame_buffer: &GpuFrameBuffer,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("LightVolume");
let mut stats = RenderPassStatistics::default();
let frame_matrix = make_viewport_matrix(viewport);
let position = view.transform_point(&Point3::from(light.position)).coords;
let color = light.color.srgb_to_linear_f32().xyz();
match light.kind {
LightSourceKind::Spot {
distance,
full_cone_angle,
..
} => {
let direction = view.transform_vector(
&(-light
.up_vector
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::z)),
);
// Draw cone into stencil buffer - it will mark pixels for further volumetric light
// calculations, it will significantly reduce amount of pixels for far lights thus
// significantly improve performance.
let k = (full_cone_angle * 0.5 + 1.0f32.to_radians()).tan() * distance;
let light_shape_matrix = Isometry3 {
rotation: graph.global_rotation(light.handle),
translation: Translation {
vector: light.position,
},
}
.to_homogeneous()
* Matrix4::new_nonuniform_scaling(&Vector3::new(k, distance, k));
let mvp = view_proj * light_shape_matrix;
// Clear stencil only.
frame_buffer.clear(viewport, None, None, Some(0));
let properties = PropertyGroup::from([property("worldViewProjection", &mvp)]);
let material = RenderMaterial::from([binding("properties", &properties)]);
stats += renderer_resources.shaders.volume_marker_vol.run_pass(
1,
&ImmutableString::new("Primary"),
frame_buffer,
&self.cone,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
// Finally draw fullscreen quad, GPU will calculate scattering only on pixels that were
// marked in stencil buffer. For distant lights it will be very low amount of pixels and
// so distant lights won't impact performance.
let cone_angle_cos = (full_cone_angle * 0.5).cos();
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("invProj", &inv_proj),
property("lightPosition", &position),
property("lightDirection", &direction),
property("lightColor", &color),
property("scatterFactor", &light.scatter),
property("intensity", &light.intensity),
property("coneAngleCos", &cone_angle_cos),
]);
let material = RenderMaterial::from([
binding(
"depthSampler",
(gbuffer.depth(), &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.spot_light_volume.run_pass(
1,
&ImmutableString::new("Primary"),
frame_buffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
}
LightSourceKind::Point { radius, .. } => {
frame_buffer.clear(viewport, None, None, Some(0));
// Radius bias is used to slightly increase sphere radius to add small margin
// for fadeout effect. It is set to 5%.
let bias = 1.05;
let k = bias * radius;
let light_shape_matrix = Matrix4::new_translation(&light.position)
* Matrix4::new_nonuniform_scaling(&Vector3::new(k, k, k));
let mvp = view_proj * light_shape_matrix;
let properties = PropertyGroup::from([property("worldViewProjection", &mvp)]);
let material = RenderMaterial::from([binding("properties", &properties)]);
stats += renderer_resources.shaders.volume_marker_vol.run_pass(
1,
&ImmutableString::new("Primary"),
frame_buffer,
&self.sphere,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
// Finally draw fullscreen quad, GPU will calculate scattering only on pixels that were
// marked in stencil buffer. For distant lights it will be very low amount of pixels and
// so distant lights won't impact performance.
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("invProj", &inv_proj),
property("lightPosition", &position),
property("lightColor", &color),
property("scatterFactor", &light.scatter),
property("intensity", &light.intensity),
property("lightRadius", &radius),
]);
let material = RenderMaterial::from([
binding(
"depthSampler",
(gbuffer.depth(), &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.point_light_volume.run_pass(
1,
&ImmutableString::new("Primary"),
frame_buffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
}
_ => (),
}
Ok(stats)
}
}
File diff suppressed because it is too large Load Diff
+233
View File
@@ -0,0 +1,233 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! An observer holds all the information required to render a scene from a particular point of view.
//! Contains all information for rendering, effectively decouples rendering entities from scene
//! entities. See [`Observer`] docs for more info.
use crate::{
core::{
algebra::{Matrix4, Point3, Vector2, Vector3},
math::{frustum::Frustum, Rect},
pool::Handle,
},
graphics::gpu_texture::CubeMapFace,
renderer::utils::CubeMapFaceDescriptor,
scene::{
camera::{Camera, ColorGradingLut, Exposure, PerspectiveProjection, Projection},
collider::BitMask,
node::Node,
probe::ReflectionProbe,
EnvironmentLightingSource, Scene,
},
};
use fyrox_core::color::Color;
use fyrox_texture::TextureResource;
/// Observer position contains all the data, that describes an observer position in 3D space. It
/// could be a real camera, light source's "virtual camera" that is used for shadow mapping, etc.
#[derive(Clone, Default)]
pub struct ObserverPosition {
/// World-space position of the observer.
pub translation: Vector3<f32>,
/// Position of the near clipping plane.
pub z_near: f32,
/// Position of the far clipping plane.
pub z_far: f32,
/// The view matrix of the observer.
pub view_matrix: Matrix4<f32>,
/// Projection matrix of the observer.
pub projection_matrix: Matrix4<f32>,
/// Combination of the view and projection matrix.
pub view_projection_matrix: Matrix4<f32>,
}
impl ObserverPosition {
/// Creates a new observer position from a scene camera.
pub fn from_camera(camera: &Camera) -> Self {
Self {
translation: camera.global_position(),
z_near: camera.projection().z_near(),
z_far: camera.projection().z_far(),
view_matrix: camera.view_matrix(),
projection_matrix: camera.projection_matrix(),
view_projection_matrix: camera.view_projection_matrix(),
}
}
}
/// Collections of observers in a scene.
#[derive(Default)]
pub struct ObserversCollection {
/// Camera observers.
pub cameras: Vec<Observer>,
/// Reflection probes, rendered first.
pub reflection_probes: Vec<Observer>,
}
impl ObserversCollection {
/// Creates a new observers collection from a scene. This method collects all observers that
/// need to render the scene (which includes camera and reflection probes).
pub fn from_scene(scene: &Scene, frame_size: Vector2<f32>) -> Self {
let mut observers = Self::default();
for node in scene.graph.linear_iter() {
if node.is_globally_enabled() {
if let Some(camera) = node.cast::<Camera>() {
if camera.is_enabled() {
observers
.cameras
.push(Observer::from_camera(camera, frame_size));
}
} else if let Some(probe) = node.cast::<ReflectionProbe>() {
if probe.updated.get() {
continue;
}
probe.updated.set(true);
let projection = Projection::Perspective(PerspectiveProjection {
fov: 90.0f32.to_radians(),
z_near: *probe.z_near,
z_far: *probe.z_far,
});
let resolution = probe.resolution() as f32;
let cube_size = Vector2::repeat(probe.resolution() as f32);
let projection_matrix = projection.matrix(cube_size);
for cube_face in CubeMapFaceDescriptor::cube_faces() {
let translation = probe.global_rendering_position();
let view_matrix = Matrix4::look_at_rh(
&Point3::from(translation),
&Point3::from(translation + cube_face.look),
&cube_face.up,
);
let view_projection_matrix = projection_matrix * view_matrix;
observers.reflection_probes.push(Observer {
handle: node.handle(),
reflection_probe_data: Some(ReflectionProbeData {
cube_map_face: cube_face.face,
environment_lighting_source: *probe.environment_lighting_source,
ambient_lighting_color: *probe.ambient_lighting_color,
}),
render_target: Some(probe.render_target().clone()),
position: ObserverPosition {
translation,
z_near: *probe.z_near,
z_far: *probe.z_far,
view_matrix,
projection_matrix,
view_projection_matrix,
},
environment_map: None,
render_mask: *probe.render_mask,
projection: projection.clone(),
color_grading_lut: None,
color_grading_enabled: false,
exposure: Default::default(),
viewport: Rect::new(0, 0, resolution as i32, resolution as i32),
frustum: Frustum::from_view_projection_matrix(view_projection_matrix)
.unwrap_or_default(),
hdr_adaptation_speed: 1.0,
})
}
}
}
}
observers
}
}
/// The data used by the renderer when it's rendering a reflection probe.
pub struct ReflectionProbeData {
/// Cube map face of a cube render target to which to render a scene.
pub cube_map_face: CubeMapFace,
/// Environment lighting source of the reflection probe. See [`EnvironmentLightingSource`] docs
/// for more info.
pub environment_lighting_source: EnvironmentLightingSource,
/// Ambient lighting color of the reflection probe.
pub ambient_lighting_color: Color,
}
/// An observer holds all the information required to render a scene from a particular point of view.
/// Contains all information for rendering, effectively decouples rendering entities from scene
/// entities. Observer can be constructed from an arbitrary set of data or from scene entities,
/// such as cameras, reflection probes.
pub struct Observer {
/// The handle of a scene node (camera, reflection probe, etc.) that was used to create this
/// Observer.
pub handle: Handle<Node>,
/// Additional data used by reflection probes only.
pub reflection_probe_data: Option<ReflectionProbeData>,
/// Render target to which to render the scene.
pub render_target: Option<TextureResource>,
/// Position of the observer. See [`ObserverPosition`] docs for more info.
pub position: ObserverPosition,
/// Environment map which will be used for IBL and reflections. If not set, then scene's skybox
/// will be used as an environment map.
pub environment_map: Option<TextureResource>,
/// A set of switches that defines which "layers" of the scene will be rendered.
pub render_mask: BitMask,
/// Projection mode that will be used to project the scene on screen's 2D plane.
pub projection: Projection,
/// Optional color grading lookup table. See [`ColorGradingLut`] docs for more info.
pub color_grading_lut: Option<ColorGradingLut>,
/// A flag, that defines whether the color grading enabled or not.
pub color_grading_enabled: bool,
/// Exposure settings that will be applied to scene's HDR image to convert it to the final
/// low dynamic range image that will be shown on a display.
pub exposure: Exposure,
/// Viewport rectangle in screen space. Defines a porting of the screen that needs to be rendered.
pub viewport: Rect<i32>,
/// Frustum of the observer, it can be used for frustum culling.
pub frustum: Frustum,
/// Defines the speed of automatic adaptation for the current frame luminance. In other words,
/// it defines how fast the reaction to the new frame brightness will be. The lower the value,
/// the longer it will take to adjust the exposure for the new brightness level.
pub hdr_adaptation_speed: f32,
}
impl Observer {
/// Creates a new observer from a scene camera.
pub fn from_camera(camera: &Camera, mut frame_size: Vector2<f32>) -> Self {
if let Some(render_target) = camera.render_target() {
if let Some(size) = render_target
.data_ref()
.as_loaded_ref()
.and_then(|rt| rt.kind().rectangle_size().map(|size| size.cast::<f32>()))
{
frame_size = size;
}
}
Observer {
handle: camera.handle(),
environment_map: camera.environment_map(),
render_mask: *camera.render_mask,
projection: camera.projection().clone(),
position: ObserverPosition::from_camera(camera),
render_target: camera.render_target().cloned(),
color_grading_lut: camera.color_grading_lut(),
color_grading_enabled: camera.color_grading_enabled(),
exposure: camera.exposure(),
viewport: camera.viewport_pixels(frame_size),
frustum: camera.frustum(),
reflection_probe_data: None,
hdr_adaptation_speed: camera.hdr_adaptation_speed(),
}
}
}
+133
View File
@@ -0,0 +1,133 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{algebra::Vector3, pool::Handle},
scene::node::Node,
};
use fxhash::FxHashMap;
use std::ops::{Deref, DerefMut};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Visibility {
Invisible,
Visible,
}
impl From<bool> for Visibility {
fn from(value: bool) -> Self {
match value {
true => Visibility::Visible,
false => Visibility::Invisible,
}
}
}
impl Visibility {
pub fn should_be_rendered(self) -> bool {
match self {
Visibility::Visible => true,
Visibility::Invisible => false,
}
}
}
#[derive(Debug, Default)]
pub struct NodeVisibilityMap {
map: FxHashMap<Handle<Node>, Visibility>,
}
impl NodeVisibilityMap {
pub fn mark(&mut self, node: Handle<Node>, visibility: Visibility) {
*self.map.entry(node).or_insert(visibility) = visibility;
}
pub fn is_visible(&self, node: Handle<Node>) -> bool {
self.map
.get(&node)
.is_none_or(|vis| vis.should_be_rendered())
}
pub fn needs_occlusion_query(&self, node: Handle<Node>) -> bool {
self.map
.get(&node)
.is_none_or(|vis| *vis == Visibility::Invisible)
}
}
impl Deref for NodeVisibilityMap {
type Target = FxHashMap<Handle<Node>, Visibility>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl DerefMut for NodeVisibilityMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
/// Volumetric visibility cache based on occlusion query.
#[derive(Debug)]
pub struct GridCache {
cells: FxHashMap<Vector3<i32>, NodeVisibilityMap>,
granularity: Vector3<u32>,
}
fn world_to_grid(world_position: Vector3<f32>, granularity: Vector3<u32>) -> Vector3<i32> {
Vector3::new(
(world_position.x * (granularity.x as f32)).round() as i32,
(world_position.y * (granularity.y as f32)).round() as i32,
(world_position.z * (granularity.z as f32)).round() as i32,
)
}
impl GridCache {
/// Creates new visibility cache with the given granularity and distance discard threshold.
/// Granularity in means how much the cache should subdivide the world. For example 2 means that
/// 1 meter cell will be split into 8 blocks by 0.5 meters. Distance discard threshold means how
/// far an observer can without discarding visibility info about distant objects.
pub fn new(granularity: Vector3<u32>) -> Self {
Self {
cells: Default::default(),
granularity,
}
}
/// Transforms the given world-space position into internal grid-space position.
pub fn world_to_grid(&self, world_position: Vector3<f32>) -> Vector3<i32> {
world_to_grid(world_position, self.granularity)
}
pub fn cell(&self, observer_position: Vector3<f32>) -> Option<&NodeVisibilityMap> {
self.cells.get(&self.world_to_grid(observer_position))
}
pub fn get_or_insert_cell(
&mut self,
observer_position: Vector3<f32>,
) -> &mut NodeVisibilityMap {
self.cells
.entry(self.world_to_grid(observer_position))
.or_default()
}
}
+416
View File
@@ -0,0 +1,416 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Full algorithm explained - <https://fyrox.rs/blog/post/tile-based-occlusion-culling/>
mod grid;
mod optimizer;
use crate::{
core::{
algebra::{Matrix4, Vector2, Vector3},
array_as_u8_slice,
color::Color,
math::{aabb::AxisAlignedBoundingBox, Rect, Vector3Ext},
pool::Handle,
ImmutableString,
},
graph::SceneGraph,
graphics::{
error::FrameworkError,
framebuffer::Attachment,
framebuffer::GpuFrameBuffer,
gpu_texture::GpuTexture,
gpu_texture::{GpuTextureKind, PixelKind},
server::GraphicsServer,
stats::RenderPassStatistics,
},
renderer::resources::RendererResources,
renderer::{
cache::shader::{binding, property, PropertyGroup, RenderMaterial},
cache::uniform::UniformBufferCache,
debug_renderer::{self, DebugRenderer},
occlusion::{
grid::{GridCache, Visibility},
optimizer::VisibilityBufferOptimizer,
},
storage::MatrixStorage,
},
scene::{graph::Graph, node::Node},
};
use bytemuck::{Pod, Zeroable};
pub struct OcclusionTester {
framebuffer: GpuFrameBuffer,
visibility_mask: GpuTexture,
tile_buffer: GpuTexture,
frame_size: Vector2<usize>,
tile_size: usize,
w_tiles: usize,
h_tiles: usize,
visibility_buffer_optimizer: VisibilityBufferOptimizer,
matrix_storage: MatrixStorage,
objects_to_test: Vec<Handle<Node>>,
view_projection: Matrix4<f32>,
observer_position: Vector3<f32>,
pub grid_cache: GridCache,
tiles: TileBuffer,
}
const MAX_BITS: usize = u32::BITS as usize;
#[derive(Default, Pod, Zeroable, Copy, Clone, Debug)]
#[repr(C)]
struct Tile {
count: u32,
objects: [u32; MAX_BITS],
}
impl Tile {
fn add(&mut self, index: u32) {
let count = self.count as usize;
if count < self.objects.len() {
self.objects[count] = index;
self.count += 1;
}
}
}
#[derive(Default, Debug)]
struct TileBuffer {
tiles: Vec<Tile>,
}
impl TileBuffer {
fn new(width: usize, height: usize) -> Self {
Self {
tiles: vec![Default::default(); width * height],
}
}
fn clear(&mut self) {
for tile in self.tiles.iter_mut() {
tile.count = 0;
}
}
}
fn inflated_world_aabb(graph: &Graph, object: Handle<Node>) -> Option<AxisAlignedBoundingBox> {
let mut aabb = graph
.try_get_node(object)
.ok()
.map(|node_ref| node_ref.world_bounding_box())?;
aabb.inflate(Vector3::repeat(0.01));
Some(aabb)
}
impl OcclusionTester {
pub fn new(
server: &dyn GraphicsServer,
width: usize,
height: usize,
tile_size: usize,
) -> Result<Self, FrameworkError> {
let depth_stencil = server.create_2d_render_target(
"OcclusionTesterDepthStencilTexture",
PixelKind::D24S8,
width,
height,
)?;
let visibility_mask = server.create_2d_render_target(
"OcclusionTesterVisibilityMask",
PixelKind::RGBA8,
width,
height,
)?;
let w_tiles = width / tile_size + 1;
let h_tiles = height / tile_size + 1;
let tile_buffer = server.create_2d_render_target(
"OcclusionTesterTileBuffer",
PixelKind::R32UI,
w_tiles * (MAX_BITS + 1),
h_tiles,
)?;
Ok(Self {
framebuffer: server.create_frame_buffer(
Some(Attachment::depth_stencil(depth_stencil)),
vec![Attachment::color(visibility_mask.clone())],
)?,
visibility_mask,
frame_size: Vector2::new(width, height),
tile_size,
w_tiles,
tile_buffer,
h_tiles,
visibility_buffer_optimizer: VisibilityBufferOptimizer::new(server, w_tiles, h_tiles)?,
matrix_storage: MatrixStorage::new(server)?,
objects_to_test: Default::default(),
view_projection: Default::default(),
observer_position: Default::default(),
grid_cache: GridCache::new(Vector3::repeat(1)),
tiles: TileBuffer::new(w_tiles, h_tiles),
})
}
pub fn try_query_visibility_results(&mut self, graph: &Graph) {
let Some(visibility_buffer) = self.visibility_buffer_optimizer.read_visibility_mask()
else {
return;
};
let mut objects_visibility = vec![false; self.objects_to_test.len()];
for y in 0..self.h_tiles {
let img_y = self.h_tiles.saturating_sub(1) - y;
let tile_offset = y * self.w_tiles;
let img_offset = img_y * self.w_tiles;
for x in 0..self.w_tiles {
let tile = &self.tiles.tiles[tile_offset + x];
let bits = visibility_buffer[img_offset + x];
let count = tile.count as usize;
for bit in 0..count {
let object_index = tile.objects[bit];
let visibility = &mut objects_visibility[object_index as usize];
let mask = 1 << bit;
let is_visible = (bits & mask) == mask;
if is_visible {
*visibility = true;
}
}
}
}
let cell = self.grid_cache.get_or_insert_cell(self.observer_position);
for (obj, vis) in self.objects_to_test.iter().zip(objects_visibility.iter()) {
cell.mark(*obj, (*vis).into());
}
for (object, visibility) in cell.iter_mut() {
let Some(aabb) = inflated_world_aabb(graph, *object) else {
continue;
};
if aabb.is_contains_point(self.observer_position) {
*visibility = Visibility::Visible;
}
}
}
fn screen_space_to_tile_space(&self, pos: Vector2<f32>) -> Vector2<usize> {
let x = (pos.x / (self.tile_size as f32)) as usize;
let y = (pos.y / (self.tile_size as f32)) as usize;
Vector2::new(x, y)
}
fn prepare_tiles(
&mut self,
graph: &Graph,
viewport: &Rect<i32>,
debug_renderer: Option<&mut DebugRenderer>,
) -> Result<(), FrameworkError> {
self.tiles.clear();
let mut lines = Vec::new();
for (object_index, object) in self.objects_to_test.iter().enumerate() {
let object_index = object_index as u32;
let Ok(node_ref) = graph.try_get_node(*object) else {
continue;
};
let aabb = node_ref.world_bounding_box();
let rect = aabb.project(&self.view_projection, viewport);
if debug_renderer.is_some() {
debug_renderer::draw_rect(&rect, &mut lines, Color::WHITE);
}
let min = self.screen_space_to_tile_space(rect.left_top_corner());
let max = self.screen_space_to_tile_space(rect.right_bottom_corner());
for y in min.y..=max.y {
let offset = y * self.w_tiles;
for x in min.x..=max.x {
self.tiles.tiles[offset + x].add(object_index);
}
}
}
if let Some(debug_renderer) = debug_renderer {
for (tile_index, tile) in self.tiles.tiles.iter().enumerate() {
let x = (tile_index % self.w_tiles) * self.tile_size;
let y = (tile_index / self.w_tiles) * self.tile_size;
let bounds = Rect::new(
x as f32,
y as f32,
self.tile_size as f32,
self.tile_size as f32,
);
debug_renderer::draw_rect(
&bounds,
&mut lines,
Color::COLORS[tile.objects.len() + 2],
);
}
debug_renderer.set_lines(&lines);
}
self.tile_buffer.set_data(
GpuTextureKind::Rectangle {
width: self.w_tiles * (MAX_BITS + 1),
height: self.h_tiles,
},
PixelKind::R32UI,
1,
Some(array_as_u8_slice(self.tiles.tiles.as_slice())),
)?;
Ok(())
}
fn upload_data<'a>(
&mut self,
graph: &Graph,
objects_to_test: impl Iterator<Item = &'a Handle<Node>>,
prev_framebuffer: &GpuFrameBuffer,
observer_position: Vector3<f32>,
view_projection: Matrix4<f32>,
) {
self.view_projection = view_projection;
self.observer_position = observer_position;
let w = self.frame_size.x as i32;
let h = self.frame_size.y as i32;
prev_framebuffer.blit_to(
&self.framebuffer,
0,
0,
w,
h,
0,
0,
w,
h,
false,
true,
false,
);
self.objects_to_test.clear();
if let Some(cell) = self.grid_cache.cell(self.observer_position) {
for object in objects_to_test {
if cell.needs_occlusion_query(*object) {
self.objects_to_test.push(*object);
}
}
}
self.objects_to_test.sort_unstable_by_key(|a| {
(graph[*a].global_position().sqr_distance(&observer_position) * 1000.0) as u64
});
}
pub fn try_run_visibility_test<'a>(
&mut self,
server: &dyn GraphicsServer,
graph: &Graph,
debug_renderer: Option<&mut DebugRenderer>,
objects_to_test: impl Iterator<Item = &'a Handle<Node>>,
prev_framebuffer: &GpuFrameBuffer,
observer_position: Vector3<f32>,
view_projection: Matrix4<f32>,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("VisibilityTest");
let mut stats = RenderPassStatistics::default();
if self.visibility_buffer_optimizer.is_reading_from_gpu() {
return Ok(stats);
}
self.upload_data(
graph,
objects_to_test,
prev_framebuffer,
observer_position,
view_projection,
);
let w = self.frame_size.x as i32;
let h = self.frame_size.y as i32;
let viewport = Rect::new(0, 0, w, h);
self.framebuffer
.clear(viewport, Some(Color::TRANSPARENT), None, None);
self.prepare_tiles(graph, &viewport, debug_renderer)?;
self.matrix_storage
.upload(self.objects_to_test.iter().filter_map(|h| {
let aabb = inflated_world_aabb(graph, *h)?;
let s = aabb.max - aabb.min;
Some(Matrix4::new_translation(&aabb.center()) * Matrix4::new_nonuniform_scaling(&s))
}))?;
let tile_size = self.tile_size as i32;
let frame_buffer_height = self.frame_size.y as f32;
let properties = PropertyGroup::from([
property("viewProjection", &self.view_projection),
property("tileSize", &tile_size),
property("frameBufferHeight", &frame_buffer_height),
]);
let material = RenderMaterial::from([
binding(
"matrices",
(
self.matrix_storage.texture(),
&renderer_resources.nearest_clamp_sampler,
),
),
binding(
"tileBuffer",
(&self.tile_buffer, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.visibility.run_pass(
self.objects_to_test.len(),
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.cube,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
self.visibility_buffer_optimizer.optimize(
server,
&self.visibility_mask,
self.tile_size as i32,
uniform_buffer_cache,
renderer_resources,
)?;
Ok(stats)
}
}
@@ -0,0 +1,130 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{color::Color, math::Rect, ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
read_buffer::GpuAsyncReadBuffer,
server::GraphicsServer,
stats::RenderPassStatistics,
},
renderer::resources::RendererResources,
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
make_viewport_matrix,
},
};
pub struct VisibilityBufferOptimizer {
framebuffer: GpuFrameBuffer,
pixel_buffer: GpuAsyncReadBuffer,
w_tiles: usize,
h_tiles: usize,
}
impl VisibilityBufferOptimizer {
pub fn new(
server: &dyn GraphicsServer,
w_tiles: usize,
h_tiles: usize,
) -> Result<Self, FrameworkError> {
let optimized_visibility_buffer = server.create_2d_render_target(
"OptimizedVisibilityTexture",
PixelKind::R32UI,
w_tiles,
h_tiles,
)?;
Ok(Self {
framebuffer: server
.create_frame_buffer(None, vec![Attachment::color(optimized_visibility_buffer)])?,
pixel_buffer: server.create_async_read_buffer(
"OcclusionReadBuffer",
size_of::<u32>(),
w_tiles * h_tiles,
)?,
w_tiles,
h_tiles,
})
}
pub fn is_reading_from_gpu(&self) -> bool {
self.pixel_buffer.is_request_running()
}
pub fn read_visibility_mask(&mut self) -> Option<Vec<u32>> {
self.pixel_buffer.try_read_of_type()
}
pub fn optimize(
&mut self,
server: &dyn GraphicsServer,
visibility_buffer: &GpuTexture,
tile_size: i32,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("VisibilityOptimizer");
let mut stats = RenderPassStatistics::default();
let viewport = Rect::new(0, 0, self.w_tiles as i32, self.h_tiles as i32);
self.framebuffer
.clear(viewport, Some(Color::TRANSPARENT), None, None);
let matrix = make_viewport_matrix(viewport);
let properties = PropertyGroup::from([
property("viewProjection", &matrix),
property("tileSize", &tile_size),
]);
let material = RenderMaterial::from([
binding(
"visibilityBuffer",
(visibility_buffer, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.visibility_optimizer.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
self.pixel_buffer
.schedule_pixels_transfer(&*self.framebuffer, 0, None)?;
Ok(stats)
}
}
+376
View File
@@ -0,0 +1,376 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! A set of textures of certain kinds. See [`RendererResources`] docs for more info.
use crate::{
core::{algebra::Matrix4, array_as_u8_slice},
graphics::{
buffer::GpuBufferDescriptor,
buffer::{BufferKind, BufferUsage, GpuBuffer},
error::FrameworkError,
geometry_buffer::GpuGeometryBuffer,
gpu_program::SamplerFallback,
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
sampler::{
GpuSampler, GpuSamplerDescriptor, MagnificationFilter, MinificationFilter, WrapMode,
},
server::GraphicsServer,
},
renderer::{cache::shader::RenderPassContainer, framework::GeometryBufferExt},
scene::mesh::surface::SurfaceData,
};
use fyrox_material::shader::ShaderDefinition;
/// A set of standard shaders used by the engine.
pub struct ShadersContainer {
/// A shader that is used to draw deferred decals.
pub decal: RenderPassContainer,
/// A spotlight shader for deferred renderer.
pub spot_light: RenderPassContainer,
/// A point light shader for deferred renderer.
pub point_light: RenderPassContainer,
/// A directional light shader for deferred renderer.
pub directional_light: RenderPassContainer,
/// A ambient light shader for deferred renderer.
pub ambient_light: RenderPassContainer,
/// A shader that is used to mark pixels affected by a light source in deferred renderer.
pub volume_marker_lit: RenderPassContainer,
/// A simple shader that is used to count pixels.
pub pixel_counter: RenderPassContainer,
/// Debug shader that is used to draw debug geometry.
pub debug: RenderPassContainer,
/// Fast approximate antialiasing shader.
pub fxaa: RenderPassContainer,
/// A shader for volumetric spotlight.
pub spot_light_volume: RenderPassContainer,
/// A shader for volumetric point light.
pub point_light_volume: RenderPassContainer,
/// A shader that is used to mark pixels affected by a light source when rendering a light
/// volume.
pub volume_marker_vol: RenderPassContainer,
/// A shader that packs the raw visibility buffer into an optimized version.
pub visibility_optimizer: RenderPassContainer,
/// Screen-space ambient occlusion shader.
pub ssao: RenderPassContainer,
/// A shader that is used in visibility test for occlusion culling.
pub visibility: RenderPassContainer,
/// A shader for simple image blitting.
pub blit: RenderPassContainer,
/// A shader for eye adaptation for high dynamic range rendering.
pub hdr_adaptation: RenderPassContainer,
/// A shader for frame luminance calculations for high dynamic range rendering.
pub hdr_luminance: RenderPassContainer,
/// A shader for frame luminance downscaling for high dynamic range rendering.
pub hdr_downscale: RenderPassContainer,
/// A shader for tone mapping for high dynamic range rendering.
pub hdr_map: RenderPassContainer,
/// A shader that extracts bright pixels from an image.
pub bloom: RenderPassContainer,
/// A shader that is used to render a skybox.
pub skybox: RenderPassContainer,
/// A gaussian blur shader.
pub gaussian_blur: RenderPassContainer,
/// A simple box blur shader.
pub box_blur: RenderPassContainer,
/// User interface shader.
pub ui: RenderPassContainer,
/// Environment map specular convolution shader.
pub environment_map_specular_convolution: RenderPassContainer,
/// Environment map irradiance convolution shader.
pub environment_map_irradiance_convolution: RenderPassContainer,
}
impl ShadersContainer {
/// Creates a new shaders container.
pub fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
Ok(Self {
decal: RenderPassContainer::from_str(server, include_str!("shaders/decal.shader"))?,
spot_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_spot_light.shader"),
)?,
point_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_point_light.shader"),
)?,
directional_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_directional_light.shader"),
)?,
ambient_light: RenderPassContainer::from_str(
server,
include_str!("shaders/ambient_light.shader"),
)?,
volume_marker_lit: RenderPassContainer::from_str(
server,
include_str!("shaders/volume_marker_lit.shader"),
)?,
pixel_counter: RenderPassContainer::from_str(
server,
include_str!("shaders/pixel_counter.shader"),
)?,
debug: RenderPassContainer::from_str(server, include_str!("shaders/debug.shader"))?,
fxaa: RenderPassContainer::from_str(server, include_str!("shaders/fxaa.shader"))?,
spot_light_volume: RenderPassContainer::from_str(
server,
include_str!("shaders/spot_volumetric.shader"),
)?,
point_light_volume: RenderPassContainer::from_str(
server,
include_str!("shaders/point_volumetric.shader"),
)?,
volume_marker_vol: RenderPassContainer::from_str(
server,
include_str!("shaders/volume_marker_vol.shader"),
)?,
visibility_optimizer: RenderPassContainer::from_str(
server,
include_str!("shaders/visibility_optimizer.shader"),
)?,
ssao: RenderPassContainer::from_str(server, include_str!("shaders/ssao.shader"))?,
visibility: RenderPassContainer::from_str(
server,
include_str!("shaders/visibility.shader"),
)?,
blit: RenderPassContainer::from_str(server, include_str!("shaders/blit.shader"))?,
hdr_adaptation: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_adaptation.shader"),
)?,
hdr_luminance: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_luminance.shader"),
)?,
hdr_downscale: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_downscale.shader"),
)?,
hdr_map: RenderPassContainer::from_str(server, include_str!("shaders/hdr_map.shader"))?,
bloom: RenderPassContainer::from_str(server, include_str!("shaders/bloom.shader"))?,
skybox: RenderPassContainer::from_str(server, include_str!("shaders/skybox.shader"))?,
gaussian_blur: RenderPassContainer::from_str(
server,
include_str!("shaders/gaussian_blur.shader"),
)?,
box_blur: RenderPassContainer::from_str(server, include_str!("shaders/blur.shader"))?,
ui: RenderPassContainer::from_str(
server,
str::from_utf8(
fyrox_material::shader::STANDARD_WIDGET
.data_source
.as_ref()
.unwrap()
.bytes
.as_ref(),
)
.unwrap(),
)?,
environment_map_specular_convolution: RenderPassContainer::from_str(
server,
include_str!("shaders/prefilter.shader"),
)?,
environment_map_irradiance_convolution: RenderPassContainer::from_str(
server,
include_str!("shaders/irradiance.shader"),
)?,
})
}
}
/// A set of textures of certain kinds that could be used as a stub in cases when you don't have
/// your own texture of this kind.
pub struct RendererResources {
/// White, one pixel, texture which will be used as stub when rendering something without
/// a texture specified.
pub white_dummy: GpuTexture,
/// Black, one pixel, texture.
pub black_dummy: GpuTexture,
/// A cube map with 6 textures of 1x1 black pixel in size.
pub environment_dummy: GpuTexture,
/// One pixel texture with (0, 1, 0) vector is used as stub when rendering something without a
/// normal map.
pub normal_dummy: GpuTexture,
/// One pixel texture used as stub when rendering something without a metallic texture. Default
/// metalness is 0.0
pub metallic_dummy: GpuTexture,
/// One pixel volume texture.
pub volume_dummy: GpuTexture,
/// A stub uniform buffer for situation when there's no actual bone matrices.
pub bone_matrices_stub_uniform_buffer: GpuBuffer,
/// A sampler with the linear filtration that clamps incoming UVs to `[0;1]` range.
pub linear_clamp_sampler: GpuSampler,
/// A sampler with the linear filtration and mipmapping that clamps incoming UVs to `[0;1]` range.
pub linear_mipmap_linear_clamp_sampler: GpuSampler,
/// A sampler with the linear filtration.
pub linear_wrap_sampler: GpuSampler,
/// A sampler with the nearest filtration that clamps incoming UVs to `[0;1]` range.
pub nearest_clamp_sampler: GpuSampler,
/// A sampler with the nearest filtration.
pub nearest_wrap_sampler: GpuSampler,
/// Unit oXY-oriented quad.
pub quad: GpuGeometryBuffer,
/// Unit cube centered around the origin.
pub cube: GpuGeometryBuffer,
/// A set of standard shaders used by the engine.
pub shaders: ShadersContainer,
}
impl RendererResources {
/// Creates a new set of renderer resources.
pub fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
Ok(Self {
white_dummy: server.create_texture(GpuTextureDescriptor {
name: "WhiteDummy",
kind: GpuTextureKind::Rectangle {
width: 1,
height: 1,
},
pixel_kind: PixelKind::RGBA8,
data: Some(&[255u8, 255u8, 255u8, 255u8]),
..Default::default()
})?,
black_dummy: server.create_texture(GpuTextureDescriptor {
name: "BlackDummy",
kind: GpuTextureKind::Rectangle {
width: 1,
height: 1,
},
pixel_kind: PixelKind::RGBA8,
data: Some(&[0u8, 0u8, 0u8, 255u8]),
..Default::default()
})?,
environment_dummy: server.create_texture(GpuTextureDescriptor {
name: "EnvironmentDummy",
kind: GpuTextureKind::Cube { size: 1 },
pixel_kind: PixelKind::RGBA8,
data: Some(&[
0u8, 0u8, 0u8, 255u8, // pos-x
0u8, 0u8, 0u8, 255u8, // neg-x
0u8, 0u8, 0u8, 255u8, // pos-y
0u8, 0u8, 0u8, 255u8, // neg-y
0u8, 0u8, 0u8, 255u8, // pos-z
0u8, 0u8, 0u8, 255u8, // neg-z
]),
..Default::default()
})?,
normal_dummy: server.create_texture(GpuTextureDescriptor {
name: "NormalDummy",
kind: GpuTextureKind::Rectangle {
width: 1,
height: 1,
},
pixel_kind: PixelKind::RGBA8,
data: Some(&[128u8, 128u8, 255u8, 255u8]),
..Default::default()
})?,
metallic_dummy: server.create_texture(GpuTextureDescriptor {
name: "MetallicDummy",
kind: GpuTextureKind::Rectangle {
width: 1,
height: 1,
},
pixel_kind: PixelKind::RGBA8,
data: Some(&[0u8, 0u8, 0u8, 0u8]),
..Default::default()
})?,
volume_dummy: server.create_texture(GpuTextureDescriptor {
name: "VolumeDummy",
kind: GpuTextureKind::Volume {
width: 1,
height: 1,
depth: 1,
},
pixel_kind: PixelKind::RGBA8,
data: Some(&[0u8, 0u8, 0u8, 0u8]),
..Default::default()
})?,
bone_matrices_stub_uniform_buffer: {
let buffer = server.create_buffer(GpuBufferDescriptor {
name: "BoneMatricesStubBuffer",
size: ShaderDefinition::MAX_BONE_MATRICES * size_of::<Matrix4<f32>>(),
kind: BufferKind::Uniform,
usage: BufferUsage::StaticDraw,
})?;
const SIZE: usize = ShaderDefinition::MAX_BONE_MATRICES * size_of::<Matrix4<f32>>();
let zeros = [0.0; SIZE];
buffer.write_data(array_as_u8_slice(&zeros))?;
buffer
},
linear_clamp_sampler: server.create_sampler(GpuSamplerDescriptor {
min_filter: MinificationFilter::Linear,
mag_filter: MagnificationFilter::Linear,
s_wrap_mode: WrapMode::ClampToEdge,
t_wrap_mode: WrapMode::ClampToEdge,
r_wrap_mode: WrapMode::ClampToEdge,
..Default::default()
})?,
linear_mipmap_linear_clamp_sampler: server.create_sampler(GpuSamplerDescriptor {
min_filter: MinificationFilter::LinearMipMapLinear,
mag_filter: MagnificationFilter::Linear,
s_wrap_mode: WrapMode::ClampToEdge,
t_wrap_mode: WrapMode::ClampToEdge,
r_wrap_mode: WrapMode::ClampToEdge,
..Default::default()
})?,
linear_wrap_sampler: server.create_sampler(GpuSamplerDescriptor {
min_filter: MinificationFilter::Linear,
mag_filter: MagnificationFilter::Linear,
..Default::default()
})?,
nearest_clamp_sampler: server.create_sampler(GpuSamplerDescriptor {
min_filter: MinificationFilter::Nearest,
mag_filter: MagnificationFilter::Nearest,
s_wrap_mode: WrapMode::ClampToEdge,
t_wrap_mode: WrapMode::ClampToEdge,
r_wrap_mode: WrapMode::ClampToEdge,
..Default::default()
})?,
nearest_wrap_sampler: server.create_sampler(GpuSamplerDescriptor {
min_filter: MinificationFilter::Nearest,
mag_filter: MagnificationFilter::Nearest,
..Default::default()
})?,
quad: GpuGeometryBuffer::from_surface_data(
"UnitQuad",
&SurfaceData::make_unit_xy_quad(),
BufferUsage::StaticDraw,
server,
)?,
cube: GpuGeometryBuffer::from_surface_data(
"UnitCube",
&SurfaceData::make_cube(Matrix4::identity()),
BufferUsage::StaticDraw,
server,
)?,
shaders: ShadersContainer::new(server)?,
})
}
/// Picks a texture that corresponds to the actual value of the given sampler fallback.
pub fn sampler_fallback(&self, sampler_fallback: SamplerFallback) -> &GpuTexture {
match sampler_fallback {
SamplerFallback::White => &self.white_dummy,
SamplerFallback::Normal => &self.normal_dummy,
SamplerFallback::Black => &self.black_dummy,
SamplerFallback::Volume => &self.volume_dummy,
}
}
}
+396
View File
@@ -0,0 +1,396 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::core::reflect::prelude::*;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, EnumString, VariantNames};
/// Bloom effect settings.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(type_uuid = "6862fea7-10a9-4a8a-8701-c003415aa3b3")]
pub struct BloomSettings {
/// Whether to use bloom effect.
pub use_bloom: bool,
/// A threshold value for luminance of a pixel to be considered "very bright". Only pixels
/// that passed this check (>=) will be included in the bloom render target and will have the glow
/// effect.
pub threshold: f32,
}
impl Default for BloomSettings {
fn default() -> Self {
Self {
use_bloom: true,
threshold: 1.01,
}
}
}
/// Calculation method of a frame luminance for HDR rendering pipeline.
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Default,
Serialize,
Deserialize,
Reflect,
AsRefStr,
EnumString,
VariantNames,
)]
#[reflect(type_uuid = "b1994c1c-bc2f-497c-a05d-062a02b69ff1")]
pub enum LuminanceCalculationMethod {
/// Simplest and fastest luminance calculation method based on average of luminance of all
/// pixels of the frame.
#[default]
DownSampling,
/// More flexible, yet slower, luminance calculation method.
Histogram,
}
/// Settings of high dynamic range rendering pipeline.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(type_uuid = "15a2975b-0f2d-4839-ba20-951e4625ee75")]
pub struct HdrSettings {
/// Whether the HDR pipeline enabled or not.
pub use_hdr: bool,
/// Calculation method of a frame luminance for HDR rendering pipeline.
pub luminance_calculation_method: LuminanceCalculationMethod,
/// Bloom effect settings.
#[serde(default)]
pub bloom_settings: BloomSettings,
}
impl Default for HdrSettings {
fn default() -> Self {
Self {
use_hdr: true,
luminance_calculation_method: LuminanceCalculationMethod::DownSampling,
bloom_settings: BloomSettings::default(),
}
}
}
/// Quality settings allows you to find optimal balance between performance and
/// graphics quality.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect(type_uuid = "ef3c1b91-dd8a-49b9-9249-1ba4c03f7176")]
pub struct QualitySettings {
/// Point shadows
/// Size of cube map face of shadow map texture in pixels.
pub point_shadow_map_size: usize,
/// Use or not percentage close filtering (smoothing) for point shadows.
pub point_soft_shadows: bool,
/// Point shadows enabled or not.
pub point_shadows_enabled: bool,
/// Maximum distance from camera to draw shadows.
pub point_shadows_distance: f32,
/// Point shadow map precision. Allows you to select compromise between
/// quality and performance.
pub point_shadow_map_precision: ShadowMapPrecision,
/// Point shadows fade out range.
/// Specifies the distance from the camera at which point shadows start to fade out.
/// Shadows beyond this distance will gradually become less visible.
pub point_shadows_fade_out_range: f32,
/// Spot shadows
/// Size of square shadow map texture in pixels
pub spot_shadow_map_size: usize,
/// Use or not percentage close filtering (smoothing) for spot shadows.
pub spot_soft_shadows: bool,
/// Spot shadows enabled or not.
pub spot_shadows_enabled: bool,
/// Maximum distance from camera to draw shadows.
pub spot_shadows_distance: f32,
/// Spot shadow map precision. Allows you to select compromise between
/// quality and performance.
pub spot_shadow_map_precision: ShadowMapPrecision,
/// Specifies the distance from the camera at which spot shadows start to fade out.
/// Shadows beyond this distance will gradually become less visible.
pub spot_shadows_fade_out_range: f32,
/// Cascaded-shadow maps settings.
pub csm_settings: CsmSettings,
/// Whether to use screen space ambient occlusion or not.
pub use_ssao: bool,
/// Radius of sampling hemisphere used in SSAO, it defines much ambient
/// occlusion will be in your scene.
pub ssao_radius: f32,
/// Global switch to enable or disable light scattering. Each light can have
/// its own scatter switch, but this one is able to globally disable scatter.
pub light_scatter_enabled: bool,
/// Whether to use Fast Approximate AntiAliasing or not.
pub fxaa: bool,
/// Whether to use Parallax Mapping or not.
pub use_parallax_mapping: bool,
/// Whether to use occlusion culling for geometry or not. Warning: this is experimental feature
/// that may have bugs and unstable behavior. Disabled by default.
#[serde(default)]
pub use_occlusion_culling: bool,
/// Whether to use occlusion culling for light sources or not. Warning: this is experimental
/// feature that may have bugs and unstable behavior. Disabled by default.
#[serde(default)]
pub use_light_occlusion_culling: bool,
/// HDR pipeline settings.
#[serde(default)]
pub hdr_settings: HdrSettings,
}
impl Default for QualitySettings {
fn default() -> Self {
Self::high()
}
}
impl QualitySettings {
/// Highest possible graphics quality. Requires very powerful GPU.
pub fn ultra() -> Self {
Self {
point_shadow_map_size: 2048,
point_shadows_distance: 20.0,
point_shadows_enabled: true,
point_soft_shadows: true,
point_shadows_fade_out_range: 1.0,
spot_shadow_map_size: 2048,
spot_shadows_distance: 20.0,
spot_shadows_enabled: true,
spot_soft_shadows: true,
spot_shadows_fade_out_range: 1.0,
use_ssao: true,
ssao_radius: 0.5,
light_scatter_enabled: true,
point_shadow_map_precision: ShadowMapPrecision::Full,
spot_shadow_map_precision: ShadowMapPrecision::Full,
fxaa: true,
hdr_settings: Default::default(),
use_parallax_mapping: true,
csm_settings: Default::default(),
use_occlusion_culling: false,
use_light_occlusion_culling: false,
}
}
/// High graphics quality, includes all graphical effects. Requires powerful GPU.
pub fn high() -> Self {
Self {
point_shadow_map_size: 1024,
point_shadows_distance: 15.0,
point_shadows_enabled: true,
point_soft_shadows: true,
point_shadows_fade_out_range: 1.0,
spot_shadow_map_size: 1024,
spot_shadows_distance: 15.0,
spot_shadows_enabled: true,
spot_soft_shadows: true,
spot_shadows_fade_out_range: 1.0,
use_ssao: true,
ssao_radius: 0.5,
light_scatter_enabled: true,
point_shadow_map_precision: ShadowMapPrecision::Full,
spot_shadow_map_precision: ShadowMapPrecision::Full,
fxaa: true,
hdr_settings: Default::default(),
use_parallax_mapping: true,
csm_settings: CsmSettings {
enabled: true,
size: 2048,
precision: ShadowMapPrecision::Full,
pcf: true,
},
use_occlusion_culling: false,
use_light_occlusion_culling: false,
}
}
/// Medium graphics quality, some of effects are disabled, shadows will have sharp edges.
pub fn medium() -> Self {
Self {
point_shadow_map_size: 512,
point_shadows_distance: 5.0,
point_shadows_enabled: true,
point_soft_shadows: false,
point_shadows_fade_out_range: 1.0,
spot_shadow_map_size: 512,
spot_shadows_distance: 5.0,
spot_shadows_enabled: true,
spot_soft_shadows: false,
spot_shadows_fade_out_range: 1.0,
use_ssao: true,
ssao_radius: 0.5,
light_scatter_enabled: false,
point_shadow_map_precision: ShadowMapPrecision::Half,
spot_shadow_map_precision: ShadowMapPrecision::Half,
fxaa: true,
hdr_settings: Default::default(),
use_parallax_mapping: false,
csm_settings: CsmSettings {
enabled: true,
size: 512,
precision: ShadowMapPrecision::Full,
pcf: false,
},
use_occlusion_culling: false,
use_light_occlusion_culling: false,
}
}
/// Lowest graphics quality, all effects are disabled.
pub fn low() -> Self {
Self {
point_shadow_map_size: 1, // Zero is unsupported.
point_shadows_distance: 0.0,
point_shadows_enabled: false,
point_soft_shadows: false,
point_shadows_fade_out_range: 1.0,
spot_shadow_map_size: 1,
spot_shadows_distance: 0.0,
spot_shadows_enabled: false,
spot_soft_shadows: false,
spot_shadows_fade_out_range: 1.0,
use_ssao: false,
ssao_radius: 0.5,
light_scatter_enabled: false,
point_shadow_map_precision: ShadowMapPrecision::Half,
spot_shadow_map_precision: ShadowMapPrecision::Half,
fxaa: false,
hdr_settings: HdrSettings {
bloom_settings: BloomSettings {
use_bloom: false,
..Default::default()
},
..Default::default()
},
use_parallax_mapping: false,
csm_settings: CsmSettings {
enabled: true,
size: 512,
precision: ShadowMapPrecision::Half,
pcf: false,
},
use_occlusion_culling: false,
use_light_occlusion_culling: false,
}
}
}
/// Cascaded-shadow maps settings.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Reflect, Eq)]
#[reflect(type_uuid = "f3dc86da-f1d4-499a-89bb-7d784eebc32d")]
pub struct CsmSettings {
/// Whether cascaded shadow maps enabled or not.
pub enabled: bool,
/// Size of texture for each cascade.
pub size: usize,
/// Bit-wise precision for each cascade, the lower precision the better performance is,
/// but the more artifacts may occur.
pub precision: ShadowMapPrecision,
/// Whether to use Percentage-Closer Filtering or not.
pub pcf: bool,
}
impl Default for CsmSettings {
fn default() -> Self {
Self {
enabled: true,
size: 2048,
precision: ShadowMapPrecision::Full,
pcf: true,
}
}
}
/// Shadow map precision allows you to select compromise between quality and performance.
#[derive(
Copy,
Clone,
Hash,
PartialOrd,
PartialEq,
Eq,
Ord,
Debug,
Serialize,
Deserialize,
Reflect,
AsRefStr,
EnumString,
VariantNames,
)]
#[reflect(type_uuid = "f9b2755b-248e-46ba-bcab-473eac1acdb8")]
pub enum ShadowMapPrecision {
/// Shadow map will use 2 times less memory by switching to 16bit pixel format,
/// but "shadow acne" may occur.
Half,
/// Shadow map will use 32bit pixel format. This option gives highest quality,
/// but could be less performant than `Half`.
Full,
}
@@ -0,0 +1,159 @@
(
name: "AmbientLight",
resources: [
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "aoSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "bakedLightingTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "depthTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 3
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 4
),
(
name: "materialTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 5
),
(
name: "prefilteredSpecularMap",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 6
),
(
name: "irradianceMap",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 7
),
(
name: "brdfLUT",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 8
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "ambientColor", kind: Vector4()),
(name: "cameraPosition", kind: Vector3()),
(name: "invViewProj", kind: Matrix4()),
(name: "skyboxLighting", kind: Bool()),
(name: "environmentLightingBrightness", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: SrcAlpha,
dfactor: OneMinusSrcAlpha,
alpha_sfactor: SrcAlpha,
alpha_dfactor: OneMinusSrcAlpha,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
float depth = texture(depthTexture, texCoord).r;
vec3 fragmentPosition = S_UnProject(vec3(texCoord, depth), properties.invViewProj);
vec4 albedo = S_SRGBToLinear(texture(diffuseTexture, texCoord));
vec3 fragmentNormal = normalize(texture(normalTexture, texCoord).xyz * 2.0 - 1.0);
vec3 material = texture(materialTexture, texCoord).rgb;
float metallic = material.x;
float roughness = material.y;
float materialAo = material.z;
vec3 viewVector = normalize(properties.cameraPosition - fragmentPosition);
vec3 reflectionVector = -reflect(viewVector, fragmentNormal);
float clampedCosViewAngle = max(dot(fragmentNormal, viewVector), 0.0);
ivec2 cubeMapSize = textureSize(prefilteredSpecularMap, 0);
float mip = roughness * (floor(log2(float(cubeMapSize.x))) + 1.0);
vec3 reflection = properties.skyboxLighting ? S_SRGBToLinear(textureLod(prefilteredSpecularMap, reflectionVector, mip)).rgb : properties.ambientColor.rgb;
vec3 F0 = mix(vec3(0.04), albedo.rgb, metallic);
vec3 F = S_FresnelSchlickRoughness(clampedCosViewAngle, F0, roughness);
vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
vec2 envBRDF = texture(brdfLUT, vec2(clampedCosViewAngle, roughness)).rg;
vec3 specular = reflection * (F * envBRDF.x + envBRDF.y);
float ambientOcclusion = texture(aoSampler, texCoord).r * materialAo;
vec4 bakedLighting = texture(bakedLightingTexture, texCoord);
vec3 irradiance = S_SRGBToLinear(texture(irradianceMap, fragmentNormal)).rgb;
vec3 diffuse = (bakedLighting.rgb + properties.environmentLightingBrightness * (properties.skyboxLighting ? irradiance : properties.ambientColor.rgb)) * albedo.rgb;
FragColor.rgb = (kD * diffuse + specular) * ambientOcclusion;
FragColor.a = bakedLighting.a;
}
"#,
)
]
)
@@ -0,0 +1,69 @@
(
name: "Blit",
resources: [
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: true,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
FragColor = texture(diffuseTexture, texCoord);
}
"#,
)
]
)
@@ -0,0 +1,75 @@
(
name: "Bloom",
resources: [
(
name: "hdrSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "threshold", kind: Float(value: 1.01)),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 outBrightColor;
void main() {
vec3 hdrPixel = texture(hdrSampler, texCoord).rgb;
if (S_Luminance(hdrPixel) > properties.threshold) {
outBrightColor = vec4(hdrPixel, 0.0);
} else {
outBrightColor = vec4(0.0);
}
}
"#,
)
]
)
@@ -0,0 +1,80 @@
(
name: "Blur",
resources: [
(
name: "inputTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
// Simple 4x4 box blur.
out float FragColor;
in vec2 texCoord;
void main()
{
vec2 texelSize = 1.0 / vec2(textureSize(inputTexture, 0));
float result = 0.0;
for (int y = -2; y < 2; ++y)
{
for (int x = -2; x < 2; ++x)
{
vec2 offset = vec2(float(x), float(y)) * texelSize;
result += texture(inputTexture, texCoord + offset).r;
}
}
FragColor = result / 16.0;
}
"#,
)
]
)
@@ -0,0 +1,64 @@
(
name: "Debug",
resources: [
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: Some(LessOrEqual),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec4 vertexColor;
out vec4 color;
void main()
{
color = vertexColor;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec4 color;
void main()
{
FragColor = color;
}
"#,
)
]
)
@@ -0,0 +1,141 @@
(
name: "Decal",
resources: [
(
name: "sceneDepth",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "decalMask",
kind: Texture(kind: USampler2D, fallback: White),
binding: 3
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invViewProj", kind: Matrix4()),
(name: "invWorldDecal", kind: Matrix4()),
(name: "resolution", kind: Vector2()),
(name: "color", kind: Vector4()),
(name: "layerIndex", kind: UInt()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: SrcAlpha,
dfactor: OneMinusSrcAlpha,
alpha_sfactor: SrcAlpha,
alpha_dfactor: OneMinusSrcAlpha,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
out vec4 clipSpacePosition;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
clipSpacePosition = gl_Position;
}
"#,
fragment_shader:
r#"
layout (location = 0) out vec4 outDiffuseMap;
layout (location = 1) out vec4 outNormalMap;
in vec4 clipSpacePosition;
void main()
{
vec2 screenPos = clipSpacePosition.xy / clipSpacePosition.w;
vec2 texCoord = vec2(
(1.0 + screenPos.x) / 2.0 + (0.5 / properties.resolution.x),
(1.0 + screenPos.y) / 2.0 + (0.5 / properties.resolution.y)
);
uvec4 maskIndex = texture(decalMask, texCoord);
// Masking.
if (maskIndex.r != properties.layerIndex) {
discard;
}
float sceneDepth = texture(sceneDepth, texCoord).r;
vec3 sceneWorldPosition = S_UnProject(vec3(texCoord, sceneDepth), properties.invViewProj);
vec3 decalSpacePosition = (properties.invWorldDecal * vec4(sceneWorldPosition, 1.0)).xyz;
// Check if scene pixel is not inside decal bounds.
vec3 dpos = vec3(0.5) - abs(decalSpacePosition.xyz);
if (dpos.x < 0.0 || dpos.y < 0.0 || dpos.z < 0.0) {
discard;
}
vec2 decalTexCoord = decalSpacePosition.xz + 0.5;
outDiffuseMap = properties.color * texture(diffuseTexture, decalTexCoord);
vec3 fragmentTangent = dFdx(sceneWorldPosition);
vec3 fragmentBinormal = dFdy(sceneWorldPosition);
vec3 fragmentNormal = cross(fragmentTangent, fragmentBinormal);
mat3 tangentToWorld;
tangentToWorld[0] = normalize(fragmentTangent); // Tangent
tangentToWorld[1] = normalize(fragmentBinormal); // Binormal
tangentToWorld[2] = normalize(fragmentNormal); // Normal
vec3 rawNormal = (texture(normalTexture, decalTexCoord) * 2.0 - 1.0).xyz;
vec3 worldSpaceNormal = tangentToWorld * rawNormal;
outNormalMap = vec4(worldSpaceNormal * 0.5 + 0.5, outDiffuseMap.a);
}
"#,
)
]
)
@@ -0,0 +1,155 @@
(
name: "DeferredDirectionalLight",
resources: [
(
name: "depthTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "colorTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "materialTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 3
),
(
name: "shadowCascade0",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 4
),
(
name: "shadowCascade1",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 5
),
(
name: "shadowCascade2",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 6
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "viewMatrix", kind: Matrix4()),
(name: "invViewProj", kind: Matrix4()),
(name: "lightViewProjMatrices", kind: Matrix4Array(max_len: 3, value: [])),
(name: "lightColor", kind: Vector4()),
(name: "lightDirection", kind: Vector3()),
(name: "cameraPosition", kind: Vector3()),
(name: "lightIntensity", kind: Float()),
(name: "shadowsEnabled", kind: Bool()),
(name: "shadowBias", kind: Float()),
(name: "softShadows", kind: Bool()),
(name: "cascadeDistances", kind: FloatArray(max_len: 3, value: [])),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 FragColor;
// Returns **inverted** shadow factor where 1 - fully bright, 0 - fully in shadow.
float CsmGetShadow(in sampler2D sampler, in vec3 fragmentPosition, in mat4 lightViewProjMatrix)
{
float invSize = 1.0 / float(textureSize(sampler, 0).x);
return S_SpotShadowFactor(properties.shadowsEnabled, properties.softShadows,
properties.shadowBias, fragmentPosition, lightViewProjMatrix, invSize, sampler);
}
void main()
{
vec3 material = texture(materialTexture, texCoord).rgb;
vec3 fragmentPosition = S_UnProject(vec3(texCoord, texture(depthTexture, texCoord).r), properties.invViewProj);
vec4 diffuseColor = texture(colorTexture, texCoord);
TPBRContext ctx;
ctx.albedo = S_SRGBToLinear(diffuseColor).rgb;
ctx.fragmentToLight = properties.lightDirection;
ctx.fragmentNormal = normalize(texture(normalTexture, texCoord).xyz * 2.0 - 1.0);
ctx.lightColor = properties.lightColor.rgb;
ctx.metallic = material.x;
ctx.roughness = material.y;
ctx.viewVector = normalize(properties.cameraPosition - fragmentPosition);
vec3 lighting = S_PBR_CalculateLight(ctx);
float fragmentZViewSpace = abs((properties.viewMatrix * vec4(fragmentPosition, 1.0)).z);
float shadow = 1.0;
if (fragmentZViewSpace <= properties.cascadeDistances[0]) {
shadow = CsmGetShadow(shadowCascade0, fragmentPosition, properties.lightViewProjMatrices[0]);
} else if (fragmentZViewSpace <= properties.cascadeDistances[1]) {
shadow = CsmGetShadow(shadowCascade1, fragmentPosition, properties.lightViewProjMatrices[1]);
} else if (fragmentZViewSpace <= properties.cascadeDistances[2]) {
shadow = CsmGetShadow(shadowCascade2, fragmentPosition, properties.lightViewProjMatrices[2]);
}
FragColor = shadow * vec4(properties.lightIntensity * lighting, diffuseColor.a);
}
"#,
)
]
)
@@ -0,0 +1,138 @@
(
name: "DeferredPointLight",
resources: [
(
name: "depthTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "colorTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "materialTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 3
),
(
name: "pointShadowTexture",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 4
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invViewProj", kind: Matrix4()),
(name: "lightColor", kind: Vector4()),
(name: "lightPos", kind: Vector3()),
(name: "cameraPosition", kind: Vector3()),
(name: "lightRadius", kind: Float()),
(name: "shadowBias", kind: Float()),
(name: "lightIntensity", kind: Float()),
(name: "shadowAlpha", kind: Float()),
(name: "softShadows", kind: Bool()),
(name: "shadowsEnabled", kind: Bool()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: Some(StencilFunc(
func: NotEqual,
ref_value: 0,
mask: 0xFFFF_FFFF
)),
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 FragColor;
void main()
{
vec3 material = texture(materialTexture, texCoord).rgb;
vec3 fragmentPosition = S_UnProject(vec3(texCoord, texture(depthTexture, texCoord).r), properties.invViewProj);
vec3 fragmentToLight = properties.lightPos - fragmentPosition;
float distance = length(fragmentToLight);
vec4 diffuseColor = texture(colorTexture, texCoord);
TPBRContext ctx;
ctx.albedo = S_SRGBToLinear(diffuseColor).rgb;
ctx.fragmentToLight = fragmentToLight / distance;
ctx.fragmentNormal = normalize(texture(normalTexture, texCoord).xyz * 2.0 - 1.0);
ctx.lightColor = properties.lightColor.rgb;
ctx.metallic = material.x;
ctx.roughness = material.y;
ctx.viewVector = normalize(properties.cameraPosition - fragmentPosition);
vec3 lighting = S_PBR_CalculateLight(ctx);
float distanceAttenuation = S_LightDistanceAttenuation(distance, properties.lightRadius);
float shadow = S_PointShadow(
properties.shadowsEnabled, properties.softShadows, distance, properties.shadowBias, ctx.fragmentToLight, pointShadowTexture);
float finalShadow = mix(1.0, shadow, properties.shadowAlpha);
FragColor = vec4(properties.lightIntensity * distanceAttenuation * finalShadow * lighting, diffuseColor.a);
}
"#,
)
]
)
@@ -0,0 +1,158 @@
(
name: "DeferredSpotLight",
resources: [
(
name: "depthTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "colorTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "materialTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 3
),
(
name: "spotShadowTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 4
),
(
name: "cookieTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 5
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "lightViewProjMatrix", kind: Matrix4()),
(name: "invViewProj", kind: Matrix4()),
(name: "lightPos", kind: Vector3()),
(name: "lightColor", kind: Vector4()),
(name: "cameraPosition", kind: Vector3()),
(name: "lightDirection", kind: Vector3()),
(name: "lightRadius", kind: Float()),
(name: "halfHotspotConeAngleCos", kind: Float()),
(name: "halfConeAngleCos", kind: Float()),
(name: "shadowMapInvSize", kind: Float()),
(name: "shadowBias", kind: Float()),
(name: "lightIntensity", kind: Float()),
(name: "shadowAlpha", kind: Float()),
(name: "cookieEnabled", kind: Bool()),
(name: "shadowsEnabled", kind: Bool()),
(name: "softShadows", kind: Bool()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: Some(StencilFunc(
func: NotEqual,
ref_value: 0,
mask: 0xFFFF_FFFF
)),
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 FragColor;
void main()
{
vec3 material = texture(materialTexture, texCoord).rgb;
vec3 fragmentPosition = S_UnProject(vec3(texCoord, texture(depthTexture, texCoord).r), properties.invViewProj);
vec3 fragmentToLight = properties.lightPos - fragmentPosition;
float distance = length(fragmentToLight);
vec4 diffuseColor = texture(colorTexture, texCoord);
TPBRContext ctx;
ctx.albedo = S_SRGBToLinear(diffuseColor).rgb;
ctx.fragmentToLight = fragmentToLight / distance;
ctx.fragmentNormal = normalize(texture(normalTexture, texCoord).xyz * 2.0 - 1.0);
ctx.lightColor = properties.lightColor.rgb;
ctx.metallic = material.x;
ctx.roughness = material.y;
ctx.viewVector = normalize(properties.cameraPosition - fragmentPosition);
vec3 lighting = S_PBR_CalculateLight(ctx);
float distanceAttenuation = S_LightDistanceAttenuation(distance, properties.lightRadius);
float spotAngleCos = dot(properties.lightDirection, ctx.fragmentToLight);
float coneFactor = smoothstep(properties.halfConeAngleCos, properties.halfHotspotConeAngleCos, spotAngleCos);
float shadow = S_SpotShadowFactor(
properties.shadowsEnabled, properties.softShadows, properties.shadowBias, fragmentPosition,
properties.lightViewProjMatrix, properties.shadowMapInvSize, spotShadowTexture);
float finalShadow = mix(1.0, shadow, properties.shadowAlpha);
vec4 cookieAttenuation = vec4(1.0);
if (properties.cookieEnabled) {
vec2 texCoords = S_Project(fragmentPosition, properties.lightViewProjMatrix).xy;
cookieAttenuation = texture(cookieTexture, texCoords);
}
FragColor = cookieAttenuation * vec4(distanceAttenuation * properties.lightIntensity * coneFactor * finalShadow * lighting, diffuseColor.a);
}
"#,
)
]
)
+276
View File
@@ -0,0 +1,276 @@
(
name: "FXAA",
resources: [
(
name: "screenTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "inverseScreenSize", kind: Vector2()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
// NVIDIA FXAA 3.11
// Original source code by TIMOTHY LOTTES
//
// Cleaned version - https://github.com/kosua20/Rendu/blob/master/resources/common/shaders/screens/fxaa.frag
//
// Final tweaks by mrDIMAS
in vec2 texCoord;
out vec4 fragColor;
// Settings for FXAA.
#define EDGE_THRESHOLD_MIN 0.0312
#define EDGE_THRESHOLD_MAX 0.125
#define QUALITY(q) ((q) < 5 ? 1.0: ((q) > 5 ? ((q) < 10 ? 2.0: ((q) < 11 ? 4.0: 8.0)): 1.5))
#define ITERATIONS 12
#define SUBPIXEL_QUALITY 0.75
float rgb2luma(vec3 rgb) {
return sqrt(dot(rgb, vec3(0.299, 0.587, 0.114)));
}
// Performs FXAA post-process anti-aliasing as described in the Nvidia FXAA white paper and the associated shader code.
void main()
{
vec4 colorCenter = texture(screenTexture, texCoord);
// Luma at the current fragment
float lumaCenter = rgb2luma(colorCenter.rgb);
// Luma at the four direct neighbours of the current fragment.
float lumaDown = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(0, -1)).rgb);
float lumaUp = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(0, 1)).rgb);
float lumaLeft = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(-1, 0)).rgb);
float lumaRight = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(1, 0)).rgb);
// Find the maximum and minimum luma around the current fragment.
float lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
float lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));
// Compute the delta.
float lumaRange = lumaMax - lumaMin;
// If the luma variation is lower that a threshold (or if we are in a really dark area), we are not on an edge, don't perform any AA.
if (lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)) {
fragColor = colorCenter;
return;
}
// Query the 4 remaining corners lumas.
float lumaDownLeft = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(-1, -1)).rgb);
float lumaUpRight = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(1, 1)).rgb);
float lumaUpLeft = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(-1, 1)).rgb);
float lumaDownRight = rgb2luma(textureLodOffset(screenTexture, texCoord, 0.0, ivec2(1, -1)).rgb);
// Combine the four edges lumas (using intermediary variables for future computations with the same values).
float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;
// Same for corners
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;
// Compute an estimation of the gradient along the horizontal and vertical axis.
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners);
// Is the local edge horizontal or vertical ?
bool isHorizontal = (edgeHorizontal >= edgeVertical);
// Choose the step size (one pixel) accordingly.
float stepLength = isHorizontal ? properties.inverseScreenSize.y : properties.inverseScreenSize.x;
// Select the two neighboring texels lumas in the opposite direction to the local edge.
float luma1 = isHorizontal ? lumaDown : lumaLeft;
float luma2 = isHorizontal ? lumaUp : lumaRight;
// Compute gradients in this direction.
float gradient1 = luma1 - lumaCenter;
float gradient2 = luma2 - lumaCenter;
// Which direction is the steepest ?
bool is1Steepest = abs(gradient1) >= abs(gradient2);
// Gradient in the corresponding direction, normalized.
float gradientScaled = 0.25 * max(abs(gradient1), abs(gradient2));
// Average luma in the correct direction.
float lumaLocalAverage = 0.0;
if (is1Steepest) {
// Switch the direction
stepLength = -stepLength;
lumaLocalAverage = 0.5 * (luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5 * (luma2 + lumaCenter);
}
// Shift UV in the correct direction by half a pixel.
vec2 currentUv = texCoord;
if (isHorizontal) {
currentUv.y += stepLength * 0.5;
} else {
currentUv.x += stepLength * 0.5;
}
// Compute offset (for each iteration step) in the right direction.
vec2 offset = isHorizontal ? vec2(properties.inverseScreenSize.x, 0.0) : vec2(0.0, properties.inverseScreenSize.y);
// Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to step faster.
vec2 uv1 = currentUv - offset * QUALITY(0);
vec2 uv2 = currentUv + offset * QUALITY(0);
// Read the lumas at both current extremities of the exploration segment, and compute the delta wrt to the local average luma.
float lumaEnd1 = rgb2luma(textureLod(screenTexture, uv1, 0.0).rgb);
float lumaEnd2 = rgb2luma(textureLod(screenTexture, uv2, 0.0).rgb);
lumaEnd1 -= lumaLocalAverage;
lumaEnd2 -= lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
bool reached1 = abs(lumaEnd1) >= gradientScaled;
bool reached2 = abs(lumaEnd2) >= gradientScaled;
bool reachedBoth = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction.
if (!reached1) {
uv1 -= offset * QUALITY(1);
}
if (!reached2) {
uv2 += offset * QUALITY(1);
}
// If both sides have not been reached, continue to explore.
if (!reachedBoth)
{
for (int i = 2; i < ITERATIONS; i++)
{
// If needed, read luma in 1st direction, compute delta.
if (!reached1) {
lumaEnd1 = rgb2luma(textureLod(screenTexture, uv1, 0.0).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
}
// If needed, read luma in opposite direction, compute delta.
if (!reached2) {
lumaEnd2 = rgb2luma(textureLod(screenTexture, uv2, 0.0).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
}
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached1 = abs(lumaEnd1) >= gradientScaled;
reached2 = abs(lumaEnd2) >= gradientScaled;
reachedBoth = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction, with a variable quality.
if (!reached1) {
uv1 -= offset * QUALITY(i);
}
if (!reached2) {
uv2 += offset * QUALITY(i);
}
// If both sides have been reached, stop the exploration.
if (reachedBoth) { break; }
}
}
// Compute the distances to each side edge of the edge (!).
float distance1 = isHorizontal ? (texCoord.x - uv1.x) : (texCoord.y - uv1.y);
float distance2 = isHorizontal ? (uv2.x - texCoord.x) : (uv2.y - texCoord.y);
// In which direction is the side of the edge closer ?
bool isDirection1 = distance1 < distance2;
float distanceFinal = min(distance1, distance2);
// Thickness of the edge.
float edgeThickness = (distance1 + distance2);
// Is the luma at center smaller than the local average ?
bool isLumaCenterSmaller = lumaCenter < lumaLocalAverage;
// If the luma at center is smaller than at its neighbour, the delta luma at each end should be positive (same variation).
bool correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller;
bool correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller;
// Only keep the result in the direction of the closer side of the edge.
bool correctVariation = isDirection1 ? correctVariation1 : correctVariation2;
// UV offset: read in the direction of the closest side of the edge.
float pixelOffset = -distanceFinal / edgeThickness + 0.5;
// If the luma variation is incorrect, do not offset.
float finalOffset = correctVariation ? pixelOffset : 0.0;
// Sub-pixel shifting
// Full weighted average of the luma over the 3x3 neighborhood.
float lumaAverage = (1.0 / 12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
// Ratio of the delta between the global average and the center luma, over the luma range in the 3x3 neighborhood.
float subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter) / lumaRange, 0.0, 1.0);
float subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
// Compute a sub-pixel offset based on this delta.
float subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;
// Pick the biggest of the two offsets.
finalOffset = max(finalOffset, subPixelOffsetFinal);
// Compute the final UV coordinates.
vec2 finalUv = texCoord;
if (isHorizontal) {
finalUv.y += finalOffset * stepLength;
} else {
finalUv.x += finalOffset * stepLength;
}
// Read the color at the new UV coordinates, and use it.
vec3 finalColor = textureLod(screenTexture, finalUv, 0.0).rgb;
fragColor = vec4(finalColor, 1.0);
}
"#,
)
]
)
@@ -0,0 +1,93 @@
(
name: "GaussianBlur",
resources: [
(
name: "image",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "pixelSize", kind: Vector2()),
(name: "horizontal", kind: Bool()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 outColor;
void main()
{
const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
vec4 center = texture(image, texCoord);
vec3 result = center.rgb * weights[0];
if (properties.horizontal) {
for (int i = 1; i < 5; ++i) {
float fi = float(i);
result += texture(image, texCoord + vec2(properties.pixelSize.x * fi, 0.0)).rgb * weights[i];
result += texture(image, texCoord - vec2(properties.pixelSize.x * fi, 0.0)).rgb * weights[i];
}
} else {
for (int i = 1; i < 5; ++i) {
float fi = float(i);
result += texture(image, texCoord + vec2(0.0, properties.pixelSize.y * fi)).rgb * weights[i];
result += texture(image, texCoord - vec2(0.0, properties.pixelSize.y * fi)).rgb * weights[i];
}
}
outColor = vec4(result, center.a);
}
"#,
)
]
)
@@ -0,0 +1,74 @@
(
name: "HdrAdaptation",
resources: [
(
name: "oldLumSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "newLumSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "speed", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out float outLum;
void main() {
float oldLum = texture(oldLumSampler, vec2(0.5, 0.5)).r;
float newLum = texture(newLumSampler, vec2(0.5, 0.5)).r;
outLum = oldLum + (newLum - oldLum) * properties.speed;
}
"#,
)
]
)
@@ -0,0 +1,94 @@
(
name: "HdrDownscale",
resources: [
(
name: "lumSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invSize", kind: Vector2()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out float outLum;
void main() {
float x = properties.invSize.x;
float y = properties.invSize.y;
float twoX = 2.0 * x;
float twoY = 2.0 * y;
float a = texture(lumSampler, vec2(texCoord.x - twoX, texCoord.y + twoY)).r;
float b = texture(lumSampler, vec2(texCoord.x, texCoord.y + twoY)).r;
float c = texture(lumSampler, vec2(texCoord.x + twoX, texCoord.y + twoY)).r;
float d = texture(lumSampler, vec2(texCoord.x - twoX, texCoord.y)).r;
float e = texture(lumSampler, vec2(texCoord.x, texCoord.y)).r;
float f = texture(lumSampler, vec2(texCoord.x + twoX, texCoord.y)).r;
float g = texture(lumSampler, vec2(texCoord.x - twoX, texCoord.y - twoY)).r;
float h = texture(lumSampler, vec2(texCoord.x, texCoord.y - twoY)).r;
float i = texture(lumSampler, vec2(texCoord.x + twoX, texCoord.y - twoY)).r;
float j = texture(lumSampler, vec2(texCoord.x - x, texCoord.y + y)).r;
float k = texture(lumSampler, vec2(texCoord.x + x, texCoord.y + y)).r;
float l = texture(lumSampler, vec2(texCoord.x - x, texCoord.y - y)).r;
float m = texture(lumSampler, vec2(texCoord.x + x, texCoord.y - y)).r;
outLum = e * 0.125;
outLum += (a + c + g + i) * 0.03125;
outLum += (b + d + f + h) * 0.0625;
outLum += (j + k + l + m) * 0.125;
}
"#,
)
]
)
@@ -0,0 +1,75 @@
(
name: "HdrLuminance",
resources: [
(
name: "frameSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invSize", kind: Vector2()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out float outLum;
void main() {
float totalLum = 0.0;
for (float y = -0.5; y < 0.5; y += 0.5) {
for (float x = -0.5; x < 0.5; x += 0.5) {
totalLum += S_Luminance(texture(frameSampler, texCoord - vec2(x, y) * properties.invSize).xyz);
}
}
outLum = totalLum / 9.0;
}
"#,
)
]
)
@@ -0,0 +1,128 @@
(
name: "HdrMap",
resources: [
(
name: "hdrSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "lumSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "bloomSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "colorMapSampler",
kind: Texture(kind: Sampler3D, fallback: White),
binding: 3
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "useColorGrading", kind: Bool()),
(name: "minLuminance", kind: Float()),
(name: "maxLuminance", kind: Float()),
(name: "autoExposure", kind: Bool()),
(name: "fixedExposure", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
out vec4 outLdrColor;
vec3 ColorGrading(vec3 color) {
const float lutSize = 16.0;
const float a = (lutSize - 1.0) / lutSize;
const float b = 1.0 / (2.0 * lutSize);
vec3 scale = vec3(a);
vec3 offset = vec3(b);
return texture(colorMapSampler, scale * color + offset).rgb;
}
// Narkowicz 2015, "ACES Filmic Tone Mapping Curve"
float TonemapACES(float x) {
const float a = 2.51;
const float b = 0.03;
const float c = 2.43;
const float d = 0.59;
const float e = 0.14;
return (x * (a * x + b)) / (x * (c * x + d) + e);
}
void main() {
vec4 hdrColor = texture(hdrSampler, texCoord) + texture(bloomSampler, texCoord);
vec3 Yxy = S_ConvertRgbToYxy(hdrColor.rgb);
float lp;
if (properties.autoExposure) {
float avgLum = texture(lumSampler, vec2(0.5, 0.5)).r;
float clampedAvgLum = clamp(avgLum, properties.minLuminance, properties.maxLuminance);
lp = Yxy.x / (9.6 * clampedAvgLum + 0.0001);
} else {
lp = Yxy.x * properties.fixedExposure;
}
Yxy.x = TonemapACES(lp);
vec4 ldrColor = vec4(S_ConvertYxyToRgb(Yxy), hdrColor.a);
if (properties.useColorGrading) {
outLdrColor = vec4(ColorGrading(S_LinearToSRGB(ldrColor).rgb), ldrColor.a);
} else {
outLdrColor = S_LinearToSRGB(ldrColor);
}
}
"#,
)
]
)
@@ -0,0 +1,96 @@
(
name: "IrradianceShader",
resources: [
(
name: "environmentMap",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
out vec3 localPos;
void main()
{
localPos = vertexPosition;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec3 localPos;
void main()
{
vec3 N = normalize(localPos);
vec3 irradiance = vec3(0.0);
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(up, N));
up = normalize(cross(N, right));
float sampleDelta = 0.1;
float nrSamples = 0.0;
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
float cosPhi = cos(phi);
float sinPhi = sin(phi);
for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
float cosTheta = cos(theta);
float sinTheta = sin(theta);
vec3 tangentSample = vec3(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta);
vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N;
irradiance += texture(environmentMap, sampleVec).rgb * cosTheta * sinTheta;
nrSamples++;
}
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));
FragColor = vec4(irradiance, 1.0);
}
"#,
)
]
)
@@ -0,0 +1,62 @@
(
name: "PixelCounter",
resources: [
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: false,
green: false,
blue: false,
alpha: false,
),
depth_write: false,
stencil_test: Some(StencilFunc (
func: NotEqual,
ref_value: 0,
mask: 0xFFFF_FFFF,
)),
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0);
}
"#,
)
]
)
@@ -0,0 +1,111 @@
(
name: "PointVolumetric",
resources: [
(
name: "depthSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invProj", kind: Matrix4()),
(name: "lightPosition", kind: Vector3()),
(name: "lightColor", kind: Vector3()),
(name: "scatterFactor", kind: Vector3()),
(name: "intensity", kind: Float()),
(name: "lightRadius", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: Some(StencilFunc(
func: Equal,
ref_value: 0xFF,
mask: 0xFFFF_FFFF
)),
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
vec3 fragmentPosition = S_UnProject(vec3(texCoord, texture(depthSampler, texCoord).r), properties.invProj);
float fragmentDepth = length(fragmentPosition);
vec3 viewDirection = fragmentPosition / fragmentDepth;
// Find intersection
vec3 scatter = vec3(0.0);
float minDepth, maxDepth;
if (S_RaySphereIntersection(vec3(0.0), viewDirection, properties.lightPosition, properties.lightRadius, minDepth, maxDepth))
{
// Perform depth test.
if (minDepth > 0.0 || fragmentDepth > minDepth)
{
minDepth = max(minDepth, 0.0);
maxDepth = clamp(maxDepth, 0.0, fragmentDepth);
vec3 closestPoint = viewDirection * minDepth;
scatter = properties.scatterFactor * S_InScatter(closestPoint, viewDirection, properties.lightPosition, maxDepth - minDepth);
}
}
FragColor = vec4(properties.lightColor.xyz * pow(clamp(properties.intensity * scatter, 0.0, 1.0), vec3(2.2)), 1.0);
}
"#,
)
]
)
@@ -0,0 +1,128 @@
(
name: "ReflectionCubeMapPrefilter",
resources: [
(
name: "environmentMap",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "roughness", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
out vec3 localPos;
void main()
{
localPos = vertexPosition;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec3 localPos;
float RadicalInverse_VdC(uint bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064e-10; // / 0x100000000
}
vec2 Hammersley(uint i, uint N)
{
return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
float a = roughness*roughness;
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
vec3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 tangent = normalize(cross(up, N));
vec3 bitangent = cross(N, tangent);
vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sampleVec);
}
void main()
{
vec3 N = normalize(localPos);
vec3 R = N;
vec3 V = R;
const uint SAMPLE_COUNT = 64u;
float totalWeight = 0.0;
vec3 prefilteredColor = vec3(0.0);
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{
vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(Xi, N, properties.roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NdotL = max(dot(N, L), 0.0);
if(NdotL > 0.0)
{
prefilteredColor += texture(environmentMap, L).rgb * NdotL;
totalWeight += NdotL;
}
}
prefilteredColor = prefilteredColor / totalWeight;
FragColor = vec4(prefilteredColor, 1.0);
}
"#,
)
]
)
@@ -0,0 +1,68 @@
(
name: "SkyBox",
resources: [
(
name: "cubemapTexture",
kind: Texture(kind: SamplerCube, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
out vec3 texCoord;
void main()
{
texCoord = vertexPosition;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec3 texCoord;
void main()
{
FragColor = S_SRGBToLinear(texture(cubemapTexture, texCoord));
}
"#,
)
]
)
@@ -0,0 +1,135 @@
(
name: "SpotVolumetric",
resources: [
(
name: "depthSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "invProj", kind: Matrix4()),
(name: "lightPosition", kind: Vector3()),
(name: "lightDirection", kind: Vector3()),
(name: "lightColor", kind: Vector3()),
(name: "scatterFactor", kind: Vector3()),
(name: "intensity", kind: Float()),
(name: "coneAngleCos", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: Some(StencilFunc(
func: Equal,
ref_value: 0xFF,
mask: 0xFFFF_FFFF
)),
depth_test: None,
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
vec3 fragmentPosition = S_UnProject(vec3(texCoord, texture(depthSampler, texCoord).r), properties.invProj);
float fragmentDepth = length(fragmentPosition);
vec3 viewDirection = fragmentPosition / fragmentDepth;
// Ray-cone intersection
float sqrConeAngleCos = properties.coneAngleCos * properties.coneAngleCos;
vec3 CO = -properties.lightPosition;
float DdotV = dot(viewDirection, properties.lightDirection);
float COdotV = dot(CO, properties.lightDirection);
float a = DdotV * DdotV - sqrConeAngleCos;
float b = 2.0 * (DdotV * COdotV - dot(viewDirection, CO) * sqrConeAngleCos);
float c = COdotV * COdotV - dot(CO, CO) * sqrConeAngleCos;
// Find intersection
vec3 scatter = vec3(0.0);
float minDepth, maxDepth;
if (S_SolveQuadraticEq(a, b, c, minDepth, maxDepth))
{
float dt1 = dot((minDepth * viewDirection) - properties.lightPosition, properties.lightDirection);
float dt2 = dot((maxDepth * viewDirection) - properties.lightPosition, properties.lightDirection);
// Discard points on reflected cylinder and perform depth test.
if ((dt1 > 0.0 || dt2 > 0.0) && (minDepth > 0.0 || fragmentDepth > minDepth))
{
if (dt1 > 0.0 && dt2 < 0.0)
{
// Closest point is on cylinder, farthest on reflected.
maxDepth = minDepth;
minDepth = 0.0;
}
else if (dt1 < 0.0 && dt2 > 0.0)
{
// Farthest point is on cylinder, closest on reflected.
minDepth = maxDepth;
maxDepth = fragmentDepth;
}
minDepth = max(minDepth, 0.0);
maxDepth = clamp(maxDepth, 0.0, fragmentDepth);
scatter = properties.scatterFactor * S_InScatter(viewDirection * minDepth, viewDirection, properties.lightPosition, maxDepth - minDepth);
}
}
FragColor = vec4(properties.lightColor.xyz * pow(clamp(properties.intensity * scatter, 0.0, 1.0), vec3(2.2)), 1.0);
}
"#,
)
]
)
+112
View File
@@ -0,0 +1,112 @@
(
name: "SSAO",
resources: [
(
name: "depthSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "normalSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
(
name: "noiseSampler",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
(name: "inverseProjectionMatrix", kind: Matrix4()),
(name: "projectionMatrix", kind: Matrix4()),
(name: "kernel", kind: Vector3Array(max_len: 32, value: [])),
(name: "noiseScale", kind: Vector2()),
(name: "viewMatrix", kind: Matrix3()),
(name: "radius", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: None,
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out float finalOcclusion;
in vec2 texCoord;
vec3 GetViewSpacePosition(vec2 screenCoord) {
return S_UnProject(vec3(screenCoord, texture(depthSampler, screenCoord).r), properties.inverseProjectionMatrix);
}
void main() {
vec3 fragPos = GetViewSpacePosition(texCoord);
vec3 worldSpaceNormal = texture(normalSampler, texCoord).xyz * 2.0 - 1.0;
vec3 viewSpaceNormal = normalize(properties.viewMatrix * worldSpaceNormal);
vec3 randomVec = normalize(texture(noiseSampler, texCoord * properties.noiseScale).xyz * 2.0 - 1.0);
vec3 tangent = normalize(randomVec - viewSpaceNormal * dot(randomVec, viewSpaceNormal));
vec3 bitangent = normalize(cross(viewSpaceNormal, tangent));
mat3 TBN = mat3(tangent, bitangent, viewSpaceNormal);
float occlusion = 0.0;
const int kernelSize = 32;
for (int i = 0; i < kernelSize; ++i) {
vec3 samplePoint = fragPos.xyz + TBN * properties.kernel[i] * properties.radius;
vec4 offset = properties.projectionMatrix * vec4(samplePoint, 1.0);
offset.xy /= offset.w;
offset.xy = offset.xy * 0.5 + 0.5;
vec3 position = GetViewSpacePosition(offset.xy);
float rangeCheck = smoothstep(0.0, 1.0, properties.radius / abs(fragPos.z - position.z));
occlusion += rangeCheck * ((position.z > samplePoint.z + 0.04) ? 1.0 : 0.0);
}
finalOcclusion = 1.0 - occlusion / float(kernelSize);
}
"#,
)
]
)
@@ -0,0 +1,110 @@
(
name: "Visibility",
resources: [
(
name: "matrices",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "tileBuffer",
kind: Texture(kind: USampler2D, fallback: White),
binding: 1
),
(
name: "properties",
kind: PropertyGroup([
(name: "viewProjection", kind: Matrix4()),
(name: "tileSize", kind: Int()),
(name: "frameBufferHeight", kind: Float()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: Some(Back),
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: Some(LessOrEqual),
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: One,
dfactor: One,
alpha_sfactor: One,
alpha_dfactor: One,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
flat out uint objectIndex;
void main()
{
objectIndex = uint(gl_InstanceID);
gl_Position = (properties.viewProjection * S_FetchMatrix(matrices, gl_InstanceID)) * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
flat in uint objectIndex;
void main()
{
int x = int(gl_FragCoord.x) / properties.tileSize;
int y = int(properties.frameBufferHeight - gl_FragCoord.y) / properties.tileSize;
int bitIndex = -1;
int tileDataIndex = x * 33;
int count = int(texelFetch(tileBuffer, ivec2(tileDataIndex, y), 0).x);
int objectsListStartIndex = tileDataIndex + 1;
for (int i = 0; i < count; ++i) {
uint pixelObjectIndex = uint(texelFetch(tileBuffer, ivec2(objectsListStartIndex + i, y), 0).x);
if (pixelObjectIndex == objectIndex) {
bitIndex = i;
break;
}
}
if (bitIndex < 0) {
FragColor = vec4(0.0, 0.0, 0.0, 0.0);
} else {
uint outMask = uint(1 << bitIndex);
float r = float(outMask & 255u) / 255.0;
float g = float((outMask & 65280u) >> 8) / 255.0;
float b = float((outMask & 16711680u) >> 16) / 255.0;
float a = float((outMask & 4278190080u) >> 24) / 255.0;
FragColor = vec4(r, g, b, a);
}
}
"#,
)
]
)
@@ -0,0 +1,80 @@
(
name: "VisibilityOptimizer",
resources: [
(
name: "visibilityBuffer",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(name: "viewProjection", kind: Matrix4()),
(name: "tileSize", kind: Int()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: false,
stencil_test: None,
depth_test: Some(LessOrEqual),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Zero,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
void main()
{
gl_Position = properties.viewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out uint optimizedVisibilityMask;
void main()
{
int tileX = int(gl_FragCoord.x);
int tileY = int(gl_FragCoord.y);
int beginX = tileX * properties.tileSize;
int beginY = tileY * properties.tileSize;
int endX = (tileX + 1) * properties.tileSize;
int endY = (tileY + 1) * properties.tileSize;
int visibilityMask = 0;
for (int y = beginY; y < endY; ++y) {
for (int x = beginX; x < endX; ++x) {
ivec4 mask = ivec4(texelFetch(visibilityBuffer, ivec2(x, y), 0) * 255.0);
visibilityMask |= (mask.a << 24) | (mask.b << 16) | (mask.g << 8) | mask.r;
}
}
optimizedVisibilityMask = uint(visibilityMask);
}
"#,
)
]
)
@@ -0,0 +1,39 @@
(
name: "VolumeMarkerLighting",
resources: [
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
// Drawing params are dynamic.
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0);
}
"#,
)
]
)
@@ -0,0 +1,62 @@
(
name: "VolumeMarkerVolume",
resources: [
(
name: "properties",
kind: PropertyGroup([
(name: "worldViewProjection", kind: Matrix4()),
]),
binding: 0
),
],
passes: [
(
name: "Primary",
draw_parameters: DrawParameters(
cull_face: None,
color_write: ColorMask(
red: false,
green: false,
blue: false,
alpha: false,
),
depth_write: false,
stencil_test: Some(StencilFunc (
func: Equal,
ref_value: 0xFF,
mask: 0xFFFF_FFFF
)),
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Replace,
zfail: Keep,
zpass: Replace,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout (location = 0) in vec3 vertexPosition;
void main()
{
gl_Position = properties.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0);
}
"#,
)
]
)
+303
View File
@@ -0,0 +1,303 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Point3, Vector2, Vector3},
color::Color,
math::{aabb::AxisAlignedBoundingBox, frustum::Frustum, Rect},
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
bundle::{
BundleRenderContext, LightSource, LightSourceKind, RenderDataBundleStorage,
RenderDataBundleStorageOptions,
},
cache::{
geometry::GeometryCache, shader::ShaderCache, texture::TextureCache,
uniform::UniformMemoryAllocator, DynamicSurfaceCache,
},
observer::{Observer, ObserverPosition},
resources::RendererResources,
settings::ShadowMapPrecision,
RenderPassStatistics, DIRECTIONAL_SHADOW_PASS_NAME,
},
scene::{
graph::Graph,
light::directional::{FrustumSplitOptions, CSM_NUM_CASCADES},
},
};
use approx::relative_eq;
pub struct Cascade {
pub frame_buffer: GpuFrameBuffer,
pub view_proj_matrix: Matrix4<f32>,
pub z_far: f32,
}
impl Cascade {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
let depth = server.create_2d_render_target(
"CsmCascadeTexture",
match precision {
ShadowMapPrecision::Full => PixelKind::D32F,
ShadowMapPrecision::Half => PixelKind::D16,
},
size,
size,
)?;
Ok(Self {
frame_buffer: server
.create_frame_buffer(Some(Attachment::depth(depth)), Default::default())?,
view_proj_matrix: Default::default(),
z_far: 0.0,
})
}
pub fn texture(&self) -> &GpuTexture {
&self.frame_buffer.depth_attachment().unwrap().texture
}
}
pub struct CsmRenderer {
cascades: [Cascade; CSM_NUM_CASCADES],
size: usize,
precision: ShadowMapPrecision,
}
pub(crate) struct CsmRenderContext<'a, 'c> {
pub elapsed_time: f32,
pub frame_size: Vector2<f32>,
pub server: &'a dyn GraphicsServer,
pub graph: &'c Graph,
pub light: &'c LightSource,
pub observer: &'a Observer,
pub geom_cache: &'a mut GeometryCache,
pub shader_cache: &'a mut ShaderCache,
pub texture_cache: &'a mut TextureCache,
pub renderer_resources: &'a RendererResources,
pub uniform_memory_allocator: &'a mut UniformMemoryAllocator,
pub dynamic_surface_cache: &'a mut DynamicSurfaceCache,
pub resource_manager: &'a ResourceManager,
}
impl CsmRenderer {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
Ok(Self {
precision,
size,
cascades: [
Cascade::new(server, size, precision)?,
Cascade::new(server, size, precision)?,
Cascade::new(server, size, precision)?,
],
})
}
pub fn precision(&self) -> ShadowMapPrecision {
self.precision
}
pub fn size(&self) -> usize {
self.size
}
pub fn cascades(&self) -> &[Cascade] {
&self.cascades
}
pub(crate) fn render(
&mut self,
ctx: CsmRenderContext,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = ctx.server.begin_scope("CascadedShadowMap");
let mut stats = RenderPassStatistics::default();
let CsmRenderContext {
elapsed_time,
frame_size,
server,
graph,
light,
observer,
geom_cache,
shader_cache,
texture_cache,
renderer_resources,
uniform_memory_allocator,
dynamic_surface_cache,
resource_manager,
} = ctx;
let LightSourceKind::Directional { ref csm_options } = light.kind else {
return Ok(stats);
};
let light_direction = -light
.up_vector
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::y);
let light_up_vec = light
.look_vector
.try_normalize(f32::EPSILON)
.unwrap_or_else(Vector3::z);
let z_values = match csm_options.split_options {
FrustumSplitOptions::Absolute { far_planes } => [
observer.position.z_near,
far_planes[0],
far_planes[1],
far_planes[2],
],
FrustumSplitOptions::Relative { fractions } => [
observer.position.z_near,
observer.position.z_far * fractions[0],
observer.position.z_far * fractions[1],
observer.position.z_far * fractions[2],
],
};
for i in 0..CSM_NUM_CASCADES {
let _debug_scope = ctx.server.begin_scope(&format!("Cascade {i}"));
let z_near = z_values[i];
let mut z_far = z_values[i + 1];
// Prevents z_near and z_far from being relatively equal, which would result in an invalid perspective matrix.
if relative_eq!(z_far, z_near) {
// Needs to be at least greater than f32::EPSILON to break the relative equality.
const MIN_DEPTH_DELTA: f32 = f32::EPSILON * 2.0;
z_far += MIN_DEPTH_DELTA * z_near;
}
let projection_matrix = observer
.projection
.clone()
.with_z_near(z_near)
.with_z_far(z_far)
.matrix(frame_size);
let frustum = Frustum::from_view_projection_matrix(
projection_matrix * observer.position.view_matrix,
)
.unwrap_or_default();
let center = frustum.center();
let observer_position = center + light_direction;
let light_view_matrix = Matrix4::look_at_lh(
&Point3::from(observer_position),
&Point3::from(center),
&light_up_vec,
);
let mut aabb = AxisAlignedBoundingBox::default();
for corner in frustum.corners() {
let light_space_corner = light_view_matrix
.transform_point(&Point3::from(corner))
.coords;
aabb.add_point(light_space_corner);
}
// Make sure most of the objects outside of the frustum will cast shadows.
let z_mult = 10.0;
if aabb.min.z < 0.0 {
aabb.min.z *= z_mult;
} else {
aabb.min.z /= z_mult;
}
if aabb.max.z < 0.0 {
aabb.max.z /= z_mult;
} else {
aabb.max.z *= z_mult;
}
let cascade_projection_matrix = Matrix4::new_orthographic(
aabb.min.x, aabb.max.x, aabb.min.y, aabb.max.y, aabb.min.z, aabb.max.z,
);
let light_view_projection = cascade_projection_matrix * light_view_matrix;
self.cascades[i].view_proj_matrix = light_view_projection;
self.cascades[i].z_far = z_far;
let viewport = Rect::new(0, 0, self.size as i32, self.size as i32);
let framebuffer = &self.cascades[i].frame_buffer;
framebuffer.clear(viewport, None, Some(1.0), None);
let bundle_storage = RenderDataBundleStorage::from_graph(
graph,
observer.render_mask,
elapsed_time,
&ObserverPosition {
translation: observer_position,
z_near,
z_far,
view_matrix: light_view_matrix,
projection_matrix: cascade_projection_matrix,
view_projection_matrix: cascade_projection_matrix * light_view_matrix,
},
DIRECTIONAL_SHADOW_PASS_NAME.clone(),
RenderDataBundleStorageOptions {
collect_lights: false,
},
dynamic_surface_cache,
);
stats += bundle_storage.render_to_frame_buffer(
server,
geom_cache,
shader_cache,
|_| true,
|_| true,
BundleRenderContext {
texture_cache,
render_pass_name: &DIRECTIONAL_SHADOW_PASS_NAME,
frame_buffer: framebuffer,
viewport,
uniform_memory_allocator,
resource_manager,
use_pom: false,
light_position: &Default::default(),
renderer_resources,
ambient_light: Color::WHITE, // TODO
scene_depth: None,
},
)?;
}
Ok(stats)
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![warn(clippy::too_many_arguments)]
pub mod csm;
pub mod point;
pub mod spot;
pub fn cascade_size(base_size: usize, cascade: usize) -> usize {
match cascade {
0 => base_size,
1 => (base_size / 2).max(1),
2 => (base_size / 4).max(1),
_ => unreachable!(),
}
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Point3, Vector3},
color::Color,
math::Rect,
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
},
renderer::{
bundle::{BundleRenderContext, RenderDataBundleStorage, RenderDataBundleStorageOptions},
cache::{
shader::ShaderCache, texture::TextureCache, uniform::UniformMemoryAllocator,
DynamicSurfaceCache,
},
observer::ObserverPosition,
resources::RendererResources,
settings::ShadowMapPrecision,
shadow::cascade_size,
utils::CubeMapFaceDescriptor,
GeometryCache, RenderPassStatistics, POINT_SHADOW_PASS_NAME,
},
scene::{collider::BitMask, graph::Graph},
};
pub struct PointShadowMapRenderer {
precision: ShadowMapPrecision,
cascades: [GpuFrameBuffer; 3],
size: usize,
faces: [CubeMapFaceDescriptor; 6],
}
pub(crate) struct PointShadowMapRenderContext<'a> {
pub render_mask: BitMask,
pub elapsed_time: f32,
pub server: &'a dyn GraphicsServer,
pub graph: &'a Graph,
pub light_pos: Vector3<f32>,
pub light_radius: f32,
pub geom_cache: &'a mut GeometryCache,
pub cascade: usize,
pub shader_cache: &'a mut ShaderCache,
pub texture_cache: &'a mut TextureCache,
pub renderer_resources: &'a RendererResources,
pub uniform_memory_allocator: &'a mut UniformMemoryAllocator,
pub dynamic_surface_cache: &'a mut DynamicSurfaceCache,
pub resource_manager: &'a ResourceManager,
}
impl PointShadowMapRenderer {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
fn make_cascade(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<GpuFrameBuffer, FrameworkError> {
let depth = server.create_2d_render_target(
"PointShadowMapDepthTexture",
match precision {
ShadowMapPrecision::Full => PixelKind::D32F,
ShadowMapPrecision::Half => PixelKind::D16,
},
size,
size,
)?;
let cube_map = server.create_texture(GpuTextureDescriptor {
name: "PointLightShadowCubeMap",
kind: GpuTextureKind::Cube { size },
pixel_kind: PixelKind::R16F,
..Default::default()
})?;
server.create_frame_buffer(
Some(Attachment::depth(depth)),
vec![Attachment::color(cube_map)],
)
}
Ok(Self {
precision,
cascades: [
make_cascade(server, cascade_size(size, 0), precision)?,
make_cascade(server, cascade_size(size, 1), precision)?,
make_cascade(server, cascade_size(size, 2), precision)?,
],
size,
faces: CubeMapFaceDescriptor::cube_faces(),
})
}
pub fn base_size(&self) -> usize {
self.size
}
pub fn precision(&self) -> ShadowMapPrecision {
self.precision
}
pub fn cascade_texture(&self, cascade: usize) -> &GpuTexture {
&self.cascades[cascade].color_attachments()[0].texture
}
pub(crate) fn render(
&mut self,
args: PointShadowMapRenderContext,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = args.server.begin_scope("PointShadowMap");
let mut statistics = RenderPassStatistics::default();
let PointShadowMapRenderContext {
elapsed_time,
server,
graph,
render_mask,
light_pos,
light_radius,
geom_cache,
cascade,
shader_cache,
texture_cache,
renderer_resources,
uniform_memory_allocator,
dynamic_surface_cache,
resource_manager,
} = args;
let framebuffer = &self.cascades[cascade];
let cascade_size = cascade_size(self.size, cascade);
let viewport = Rect::new(0, 0, cascade_size as i32, cascade_size as i32);
let z_near = 0.01;
let z_far = light_radius;
let light_projection_matrix =
Matrix4::new_perspective(1.0, std::f32::consts::FRAC_PI_2, z_near, z_far);
for face in self.faces.iter() {
let _debug_scope = server.begin_scope(&format!("Face {:?}", face.face));
framebuffer.set_cubemap_face(0, face.face, 0);
framebuffer.clear(viewport, Some(Color::WHITE), Some(1.0), None);
let light_look_at = light_pos + face.look;
let light_view_matrix = Matrix4::look_at_rh(
&Point3::from(light_pos),
&Point3::from(light_look_at),
&face.up,
);
let bundle_storage = RenderDataBundleStorage::from_graph(
graph,
render_mask,
elapsed_time,
&ObserverPosition {
translation: light_pos,
z_near,
z_far,
view_matrix: light_view_matrix,
projection_matrix: light_projection_matrix,
view_projection_matrix: light_projection_matrix * light_view_matrix,
},
POINT_SHADOW_PASS_NAME.clone(),
RenderDataBundleStorageOptions {
collect_lights: false,
},
dynamic_surface_cache,
);
statistics += bundle_storage.render_to_frame_buffer(
server,
geom_cache,
shader_cache,
|_| true,
|_| true,
BundleRenderContext {
texture_cache,
render_pass_name: &POINT_SHADOW_PASS_NAME,
frame_buffer: framebuffer,
viewport,
uniform_memory_allocator,
resource_manager,
use_pom: false,
light_position: &light_pos,
renderer_resources,
ambient_light: Color::WHITE, // TODO
scene_depth: None,
},
)?;
}
Ok(statistics)
}
}
+184
View File
@@ -0,0 +1,184 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Vector3},
color::Color,
math::Rect,
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
bundle::{BundleRenderContext, RenderDataBundleStorage, RenderDataBundleStorageOptions},
cache::{
shader::ShaderCache, texture::TextureCache, uniform::UniformMemoryAllocator,
DynamicSurfaceCache,
},
observer::ObserverPosition,
resources::RendererResources,
settings::ShadowMapPrecision,
shadow::cascade_size,
GeometryCache, RenderPassStatistics, SPOT_SHADOW_PASS_NAME,
},
scene::{collider::BitMask, graph::Graph},
};
pub struct SpotShadowMapRenderer {
precision: ShadowMapPrecision,
// Three "cascades" for various use cases:
// 0 - largest, for lights close to camera.
// 1 - medium, for lights with medium distance to camera.
// 2 - small, for farthest lights.
cascades: [GpuFrameBuffer; 3],
size: usize,
}
impl SpotShadowMapRenderer {
pub fn new(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<Self, FrameworkError> {
fn make_cascade(
server: &dyn GraphicsServer,
size: usize,
precision: ShadowMapPrecision,
) -> Result<GpuFrameBuffer, FrameworkError> {
let depth = server.create_2d_render_target(
"SpotShadowMapCascade",
match precision {
ShadowMapPrecision::Full => PixelKind::D32F,
ShadowMapPrecision::Half => PixelKind::D16,
},
size,
size,
)?;
server.create_frame_buffer(Some(Attachment::depth(depth)), vec![])
}
Ok(Self {
precision,
size,
cascades: [
make_cascade(server, cascade_size(size, 0), precision)?,
make_cascade(server, cascade_size(size, 1), precision)?,
make_cascade(server, cascade_size(size, 2), precision)?,
],
})
}
pub fn base_size(&self) -> usize {
self.size
}
pub fn precision(&self) -> ShadowMapPrecision {
self.precision
}
pub fn cascade_texture(&self, cascade: usize) -> &GpuTexture {
&self.cascades[cascade].depth_attachment().unwrap().texture
}
pub fn cascade_size(&self, cascade: usize) -> usize {
cascade_size(self.size, cascade)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render(
&mut self,
server: &dyn GraphicsServer,
graph: &Graph,
render_mask: BitMask,
elapsed_time: f32,
light_position: Vector3<f32>,
light_view_matrix: Matrix4<f32>,
z_near: f32,
z_far: f32,
light_projection_matrix: Matrix4<f32>,
geom_cache: &mut GeometryCache,
cascade: usize,
shader_cache: &mut ShaderCache,
texture_cache: &mut TextureCache,
renderer_resources: &RendererResources,
uniform_memory_allocator: &mut UniformMemoryAllocator,
dynamic_surface_cache: &mut DynamicSurfaceCache,
resource_manager: &ResourceManager,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("SpotShadowMap");
let mut statistics = RenderPassStatistics::default();
let framebuffer = &self.cascades[cascade];
let cascade_size = cascade_size(self.size, cascade);
let viewport = Rect::new(0, 0, cascade_size as i32, cascade_size as i32);
framebuffer.clear(viewport, None, Some(1.0), None);
let bundle_storage = RenderDataBundleStorage::from_graph(
graph,
render_mask,
elapsed_time,
&ObserverPosition {
translation: light_position,
z_near,
z_far,
view_matrix: light_view_matrix,
projection_matrix: light_projection_matrix,
view_projection_matrix: light_projection_matrix * light_view_matrix,
},
SPOT_SHADOW_PASS_NAME.clone(),
RenderDataBundleStorageOptions {
collect_lights: false,
},
dynamic_surface_cache,
);
statistics += bundle_storage.render_to_frame_buffer(
server,
geom_cache,
shader_cache,
|_| true,
|_| true,
BundleRenderContext {
texture_cache,
render_pass_name: &SPOT_SHADOW_PASS_NAME,
frame_buffer: framebuffer,
viewport,
uniform_memory_allocator,
resource_manager,
use_pom: false,
light_position: &Default::default(),
renderer_resources,
ambient_light: Color::WHITE, // TODO
scene_depth: None,
},
)?;
Ok(statistics)
}
}
+97
View File
@@ -0,0 +1,97 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{math::Rect, ImmutableString},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, DrawCallStatistics, GpuFrameBuffer},
gpu_texture::{GpuTexture, PixelKind},
server::GraphicsServer,
},
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
make_viewport_matrix,
resources::RendererResources,
},
};
pub struct Blur {
framebuffer: GpuFrameBuffer,
width: usize,
height: usize,
}
impl Blur {
pub fn new(
server: &dyn GraphicsServer,
width: usize,
height: usize,
) -> Result<Self, FrameworkError> {
let frame =
server.create_2d_render_target("BlurTexture", PixelKind::R32F, width, height)?;
Ok(Self {
framebuffer: server.create_frame_buffer(None, vec![Attachment::color(frame)])?,
width,
height,
})
}
pub fn result(&self) -> GpuTexture {
self.framebuffer.color_attachments()[0].texture.clone()
}
pub(crate) fn render(
&self,
server: &dyn GraphicsServer,
input: GpuTexture,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<DrawCallStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("BoxBlur");
let viewport = Rect::new(0, 0, self.width as i32, self.height as i32);
let wvp = make_viewport_matrix(viewport);
let properties = PropertyGroup::from([property("worldViewProjection", &wvp)]);
let material = RenderMaterial::from([
binding(
"inputTexture",
(&input, &renderer_resources.nearest_clamp_sampler),
),
binding("properties", &properties),
]);
renderer_resources.shaders.box_blur.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)
}
}
+221
View File
@@ -0,0 +1,221 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{
algebra::{Matrix3, Matrix4, Vector2, Vector3},
color::Color,
math::{lerpf, Rect},
sstorage::ImmutableString,
},
graphics::{
error::FrameworkError,
framebuffer::{Attachment, GpuFrameBuffer},
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
},
rand::Rng,
renderer::{
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial},
uniform::UniformBufferCache,
},
gbuffer::GBuffer,
make_viewport_matrix,
resources::RendererResources,
ssao::blur::Blur,
RenderPassStatistics,
},
};
mod blur;
// Keep in sync with shader define.
const KERNEL_SIZE: usize = 32;
// Size of noise texture.
const NOISE_SIZE: usize = 4;
pub struct ScreenSpaceAmbientOcclusionRenderer {
blur: Blur,
framebuffer: GpuFrameBuffer,
width: i32,
height: i32,
noise: GpuTexture,
kernel: [Vector3<f32>; KERNEL_SIZE],
radius: f32,
}
impl ScreenSpaceAmbientOcclusionRenderer {
pub fn new(
server: &dyn GraphicsServer,
frame_width: usize,
frame_height: usize,
) -> Result<Self, FrameworkError> {
// It is good balance between quality and performance, no need to do SSAO in full resolution.
// This SSAO map size reduction was taken from DOOM (2016).
let width = (frame_width / 2).max(1);
let height = (frame_height / 2).max(1);
let occlusion =
server.create_2d_render_target("SsaoTexture", PixelKind::R32F, width, height)?;
let mut rng = crate::rand::thread_rng();
Ok(Self {
blur: Blur::new(server, width, height)?,
framebuffer: server.create_frame_buffer(None, vec![Attachment::color(occlusion)])?,
width: width as i32,
height: height as i32,
kernel: {
let mut kernel = [Default::default(); KERNEL_SIZE];
for (i, v) in kernel.iter_mut().enumerate() {
let k = i as f32 / KERNEL_SIZE as f32;
let scale = lerpf(0.1, 1.0, k * k);
*v = Vector3::new(
rng.gen_range(-1.0..1.0),
rng.gen_range(-1.0..1.0),
rng.gen_range(0.0..1.0),
)
// Make sphere
.try_normalize(f32::EPSILON)
.unwrap()
// Use non-uniform distribution to shuffle points inside hemisphere.
.scale(scale);
}
kernel
},
noise: {
const RGB_PIXEL_SIZE: usize = 3;
let mut pixels = [0u8; RGB_PIXEL_SIZE * NOISE_SIZE * NOISE_SIZE];
for pixel in pixels.chunks_exact_mut(RGB_PIXEL_SIZE) {
pixel[0] = rng.gen_range(0u8..255u8); // R
pixel[1] = rng.gen_range(0u8..255u8); // G
pixel[2] = 0u8; // B
}
server.create_texture(GpuTextureDescriptor {
name: "SsaoNoise",
kind: GpuTextureKind::Rectangle {
width: NOISE_SIZE,
height: NOISE_SIZE,
},
pixel_kind: PixelKind::RGB8,
data: Some(&pixels),
..Default::default()
})?
},
radius: 0.5,
})
}
pub fn set_radius(&mut self, radius: f32) {
self.radius = radius.abs();
}
fn raw_ao_map(&self) -> GpuTexture {
self.framebuffer.color_attachments()[0].texture.clone()
}
pub fn ao_map(&self) -> GpuTexture {
self.blur.result()
}
pub(crate) fn render(
&self,
server: &dyn GraphicsServer,
gbuffer: &GBuffer,
projection_matrix: Matrix4<f32>,
view_matrix: Matrix3<f32>,
uniform_buffer_cache: &mut UniformBufferCache,
renderer_resources: &RendererResources,
) -> Result<RenderPassStatistics, FrameworkError> {
let _debug_scope = server.begin_scope("SSAO");
let mut stats = RenderPassStatistics::default();
let viewport = Rect::new(0, 0, self.width, self.height);
let frame_matrix = make_viewport_matrix(viewport);
self.framebuffer.clear(
viewport,
Some(Color::from_rgba(0, 0, 0, 0)),
Some(1.0),
None,
);
let noise_scale = Vector2::new(
self.width as f32 / NOISE_SIZE as f32,
self.height as f32 / NOISE_SIZE as f32,
);
let inv_projection = projection_matrix.try_inverse().unwrap_or_default();
let properties = PropertyGroup::from([
property("worldViewProjection", &frame_matrix),
property("inverseProjectionMatrix", &inv_projection),
property("projectionMatrix", &projection_matrix),
property("kernel", self.kernel.as_slice()),
property("noiseScale", &noise_scale),
property("viewMatrix", &view_matrix),
property("radius", &self.radius),
]);
let material = RenderMaterial::from([
binding(
"depthSampler",
(gbuffer.depth(), &renderer_resources.nearest_clamp_sampler),
),
binding(
"normalSampler",
(
gbuffer.normal_texture(),
&renderer_resources.nearest_clamp_sampler,
),
),
binding(
"noiseSampler",
(&self.noise, &renderer_resources.nearest_wrap_sampler),
),
binding("properties", &properties),
]);
stats += renderer_resources.shaders.ssao.run_pass(
1,
&ImmutableString::new("Primary"),
&self.framebuffer,
&renderer_resources.quad,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
stats += self.blur.render(
server,
self.raw_ao_map(),
uniform_buffer_cache,
renderer_resources,
)?;
Ok(stats)
}
}
+257
View File
@@ -0,0 +1,257 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Contains all entities that are used to collect rendering statistics.
use fyrox_core::instant;
use fyrox_graphics::framebuffer::DrawCallStatistics;
pub use fyrox_graphics::stats::*;
use std::fmt::{Display, Formatter};
use std::ops::AddAssign;
/// Lighting statistics.
#[derive(Debug, Copy, Clone, Default)]
pub struct LightingStatistics {
/// How many point lights were rendered.
pub point_lights_rendered: usize,
/// How many point light shadow maps were rendered.
pub point_shadow_maps_rendered: usize,
/// How many cascaded shadow maps were rendered.
pub csm_rendered: usize,
/// How many spotlights were rendered.
pub spot_lights_rendered: usize,
/// How many spotlight shadow maps were rendered.
pub spot_shadow_maps_rendered: usize,
/// How many directional lights were rendered.
pub directional_lights_rendered: usize,
}
impl AddAssign for LightingStatistics {
fn add_assign(&mut self, rhs: Self) {
self.point_lights_rendered += rhs.point_lights_rendered;
self.point_shadow_maps_rendered += rhs.point_shadow_maps_rendered;
self.spot_lights_rendered += rhs.spot_lights_rendered;
self.spot_shadow_maps_rendered += rhs.spot_shadow_maps_rendered;
self.directional_lights_rendered += rhs.directional_lights_rendered;
self.csm_rendered += rhs.csm_rendered;
}
}
impl Display for LightingStatistics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Lighting Statistics:\n\
\tPoint Lights: {}\n\
\tSpot Lights: {}\n\
\tDirectional Lights: {}\n\
\tPoint Shadow Maps: {}\n\
\tSpot Shadow Maps: {}\n\
\tSpot Shadow Maps: {}\n",
self.point_lights_rendered,
self.spot_lights_rendered,
self.directional_lights_rendered,
self.point_shadow_maps_rendered,
self.spot_shadow_maps_rendered,
self.csm_rendered
)
}
}
/// Renderer statistics for a scene.
#[derive(Debug, Copy, Clone, Default)]
pub struct SceneStatistics {
/// Shows how many pipeline state changes was made during scene rendering.
pub pipeline: PipelineStatistics,
/// Shows how many lights and shadow maps were rendered.
pub lighting: LightingStatistics,
/// Shows how many draw calls was made and how many triangles were rendered.
pub geometry: RenderPassStatistics,
}
impl Display for SceneStatistics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}\n\
{}\n\
{}\n",
self.geometry, self.lighting, self.pipeline
)
}
}
impl AddAssign<DrawCallStatistics> for SceneStatistics {
fn add_assign(&mut self, rhs: DrawCallStatistics) {
self.geometry += rhs;
}
}
impl AddAssign<PipelineStatistics> for SceneStatistics {
fn add_assign(&mut self, rhs: PipelineStatistics) {
self.pipeline += rhs;
}
}
impl AddAssign<RenderPassStatistics> for SceneStatistics {
fn add_assign(&mut self, rhs: RenderPassStatistics) {
self.geometry += rhs;
}
}
impl AddAssign<LightingStatistics> for SceneStatistics {
fn add_assign(&mut self, rhs: LightingStatistics) {
self.lighting += rhs;
}
}
/// Renderer statistics for one frame, also includes current frames per second
/// number.
#[derive(Debug, Copy, Clone)]
pub struct Statistics {
/// Shows how many pipeline state changes was made per frame.
pub pipeline: PipelineStatistics,
/// Shows how many lights and shadow maps were rendered.
pub lighting: LightingStatistics,
/// Shows how many draw calls was made and how many triangles were rendered.
pub geometry: RenderPassStatistics,
/// Real time consumed to render a frame. Time given in **seconds**.
pub pure_frame_time: f32,
/// Total time renderer took to process single frame, usually includes time the renderer spent
/// waiting until the swap of the back and front buffers (can include vsync).
/// Time given in **seconds**.
pub capped_frame_time: f32,
/// The total number of frames been rendered in one second.
pub frames_per_second: usize,
/// The total number of textures in the textures cache.
pub texture_cache_size: usize,
/// The total number of vertex+index buffers pairs in the geometry cache.
pub geometry_cache_size: usize,
/// The total number of cached dynamic buffers.
pub dynamic_surface_cache_size: usize,
/// The total number of shaders in the shaders cache.
pub shader_cache_size: usize,
/// The total number of uniform buffers in the cache.
pub uniform_buffer_cache_size: usize,
pub(super) frame_counter: usize,
pub(super) frame_start_time: instant::Instant,
pub(super) last_fps_commit_time: instant::Instant,
}
impl std::ops::AddAssign<SceneStatistics> for Statistics {
fn add_assign(&mut self, rhs: SceneStatistics) {
self.pipeline += rhs.pipeline;
self.lighting += rhs.lighting;
self.geometry += rhs.geometry;
}
}
impl Display for Statistics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let fps = self.frames_per_second;
let pure_frame_time = self.pure_frame_time * 1000.0;
let capped_frame_time = self.capped_frame_time * 1000.0;
let geometry_stats = &self.geometry;
let lighting_stats = &self.lighting;
let pipeline_stats = &self.pipeline;
let texture_cache_size = self.texture_cache_size;
let geometry_cache_size = self.geometry_cache_size;
let dynamic_surface_cache_size = self.dynamic_surface_cache_size;
let shader_cache_size = self.shader_cache_size;
let uniform_buffer_cache_size = self.uniform_buffer_cache_size;
write!(
f,
"FPS: {fps}\n\
Pure Frame Time: {pure_frame_time:.2} ms\n\
Capped Frame Time: {capped_frame_time:.2} ms\n\
{geometry_stats}\n\
{lighting_stats}\n\
{pipeline_stats}\n\
Texture Cache Size: {texture_cache_size}\n\
Geometry Cache Size: {geometry_cache_size}\n\
Dynamic Surface Cache Size: {dynamic_surface_cache_size}\n\
Shader Cache Size: {shader_cache_size}\n
Uniform Buffer Cache Size: {uniform_buffer_cache_size}\n",
)
}
}
impl std::ops::AddAssign<RenderPassStatistics> for Statistics {
fn add_assign(&mut self, rhs: RenderPassStatistics) {
self.geometry += rhs;
}
}
impl Default for Statistics {
fn default() -> Self {
Self {
pipeline: Default::default(),
lighting: Default::default(),
geometry: Default::default(),
pure_frame_time: 0.0,
capped_frame_time: 0.0,
frames_per_second: 0,
texture_cache_size: 0,
geometry_cache_size: 0,
dynamic_surface_cache_size: 0,
shader_cache_size: 0,
uniform_buffer_cache_size: 0,
frame_counter: 0,
frame_start_time: instant::Instant::now(),
last_fps_commit_time: instant::Instant::now(),
}
}
}
impl Statistics {
/// Must be called before render anything.
pub fn begin_frame(&mut self) {
self.frame_start_time = instant::Instant::now();
self.geometry = Default::default();
self.lighting = Default::default();
}
/// Must be called before SwapBuffers but after all rendering is done.
pub fn end_frame(&mut self) {
let current_time = instant::Instant::now();
self.pure_frame_time = current_time
.duration_since(self.frame_start_time)
.as_secs_f32();
self.frame_counter += 1;
if current_time
.duration_since(self.last_fps_commit_time)
.as_secs_f32()
>= 1.0
{
self.last_fps_commit_time = current_time;
self.frames_per_second = self.frame_counter;
self.frame_counter = 0;
}
}
/// Must be called after SwapBuffers to get capped frame time.
pub fn finalize(&mut self) {
self.capped_frame_time = instant::Instant::now()
.duration_since(self.frame_start_time)
.as_secs_f32();
}
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Generic, texture-based, storage for matrices with somewhat unlimited capacity.
use crate::{
core::algebra::Matrix4,
graphics::{
error::FrameworkError,
gpu_texture::{GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
},
};
/// Generic, texture-based, storage for matrices with somewhat unlimited capacity.
///
/// ## Motivation
///
/// Why it uses textures instead of SSBO? This could be done with SSBO, but it is not available on macOS because
/// SSBO was added only in OpenGL 4.3, but macOS support up to OpenGL 4.1.
pub struct MatrixStorage {
texture: GpuTexture,
matrices: Vec<Matrix4<f32>>,
}
impl MatrixStorage {
/// Creates a new matrix storage.
pub fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
let identity = [Matrix4::<f32>::identity()];
Ok(Self {
texture: server.create_texture(GpuTextureDescriptor {
name: "MatrixStorage",
kind: GpuTextureKind::Rectangle {
width: 4,
height: 1,
},
pixel_kind: PixelKind::RGBA32F,
data: Some(crate::core::array_as_u8_slice(&identity)),
..Default::default()
})?,
matrices: Default::default(),
})
}
/// Returns matrix storage texture.
pub fn texture(&self) -> &GpuTexture {
&self.texture
}
/// Updates contents of the internal texture with provided matrices.
pub fn upload(
&mut self,
matrices: impl Iterator<Item = Matrix4<f32>>,
) -> Result<(), FrameworkError> {
self.matrices.clear();
self.matrices.extend(matrices);
// Select width for the texture by restricting width at 1024 pixels.
let matrices_tex_size = 1024;
let actual_matrices_pixel_count = self.matrices.len() * 4;
let matrices_w = actual_matrices_pixel_count.min(matrices_tex_size);
let matrices_h = (actual_matrices_pixel_count as f32 / matrices_w as f32)
.ceil()
.max(1.0) as usize;
// Pad data to actual size.
for _ in 0..(((matrices_w * matrices_h) - actual_matrices_pixel_count) / 4) {
self.matrices.push(Default::default());
}
// Upload to GPU.
if matrices_w != 0 && matrices_h != 0 {
self.texture.set_data(
GpuTextureKind::Rectangle {
width: matrices_w,
height: matrices_h,
},
PixelKind::RGBA32F,
1,
Some(crate::core::array_as_u8_slice(&self.matrices)),
)?;
}
Ok(())
}
}
+529
View File
@@ -0,0 +1,529 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! See [`UiRenderer`] docs.
use crate::{
asset::{manager::ResourceManager, untyped::ResourceKind},
core::{
algebra::{Matrix4, Vector2, Vector4},
arrayvec::ArrayVec,
color::Color,
math::Rect,
some_or_continue,
sstorage::ImmutableString,
},
graphics::{
buffer::BufferUsage,
error::FrameworkError,
framebuffer::{GpuFrameBuffer, ResourceBindGroup, ResourceBinding},
geometry_buffer::{
AttributeDefinition, AttributeKind, ElementsDescriptor, GpuGeometryBuffer,
GpuGeometryBufferDescriptor, VertexBufferData, VertexBufferDescriptor,
},
gpu_program::ShaderResourceKind,
server::GraphicsServer,
uniform::StaticUniformBuffer,
BlendFactor, BlendFunc, BlendParameters, ColorMask, CompareFunc, DrawParameters,
ElementRange, ScissorBox, StencilFunc,
},
gui::{
brush::Brush,
draw::Command,
draw::{CommandTexture, DrawingContext},
},
renderer::{
bundle::{self, make_texture_binding},
cache::{
shader::{binding, property, PropertyGroup, RenderMaterial, ShaderCache},
uniform::{UniformBlockLocation, UniformBufferCache, UniformMemoryAllocator},
},
resources::RendererResources,
RenderPassStatistics, TextureCache,
},
resource::texture::{Texture, TextureKind, TexturePixelKind, TextureResource},
};
use fyrox_ui::UserInterface;
use uuid::Uuid;
/// User interface renderer allows you to render drawing context in specified render target.
pub struct UiRenderer {
geometry_buffer: GpuGeometryBuffer,
clipping_geometry_buffer: GpuGeometryBuffer,
}
/// A set of parameters to render a specified user interface drawing context.
pub struct UiRenderContext<'a, 'b, 'c> {
/// Graphics server.
pub server: &'a dyn GraphicsServer,
/// Viewport to where render the user interface.
pub viewport: Rect<i32>,
/// Frame buffer to where render the user interface.
pub frame_buffer: &'b GpuFrameBuffer,
/// Width of the frame buffer to where render the user interface.
pub frame_width: f32,
/// Height of the frame buffer to where render the user interface.
pub frame_height: f32,
/// Drawing context of a user interface.
pub drawing_context: &'c DrawingContext,
/// Renderer resources.
pub renderer_resources: &'a RendererResources,
/// GPU texture cache.
pub texture_cache: &'a mut TextureCache,
/// A reference to the cache of uniform buffers.
pub uniform_buffer_cache: &'a mut UniformBufferCache,
/// A reference to the render pass cache.
pub render_pass_cache: &'a mut ShaderCache,
/// A reference to the uniform memory allocator.
pub uniform_memory_allocator: &'a mut UniformMemoryAllocator,
/// A reference to the resource manager.
pub resource_manager: &'a ResourceManager,
}
/// Contains all the info required to render a user interface.
pub struct UiRenderInfo<'a> {
/// A reference to a user interface that needs to be rendered.
pub ui: &'a UserInterface,
/// A render target to render a user interface (UI) to. If [`None`], then the UI will be rendered
/// to the screen directly.
pub render_target: Option<TextureResource>,
/// A color that will be used to fill a render target before rendering of the UI. Ignored if the
/// render target is [`None`] and nothing will be cleared.
pub clear_color: Color,
/// A reference to the resource manager.
pub resource_manager: &'a ResourceManager,
}
fn write_uniform_blocks(
ortho: &Matrix4<f32>,
resolution: Vector2<f32>,
commands: &[Command],
uniform_memory_allocator: &mut UniformMemoryAllocator,
) -> Vec<ArrayVec<(usize, UniformBlockLocation), 8>> {
let mut block_locations = Vec::with_capacity(commands.len());
for cmd in commands {
let mut command_block_locations = ArrayVec::<(usize, UniformBlockLocation), 8>::new();
let material_data_guard = cmd.material.data_ref();
let material = some_or_continue!(material_data_guard.as_loaded_ref());
let shader_data_guard = material.shader().data_ref();
let shader = some_or_continue!(shader_data_guard.as_loaded_ref());
for resource in shader.definition.resources.iter() {
if resource.name.as_str() == "fyrox_widgetData" {
let mut raw_stops = [0.0; 16];
let mut raw_colors = [Vector4::default(); 16];
let bounds_max = cmd.bounds.right_bottom_corner();
let (gradient_origin, gradient_end) = match cmd.brush {
Brush::Solid(_) => (Vector2::default(), Vector2::default()),
Brush::LinearGradient { from, to, .. } => (from, to),
Brush::RadialGradient { center, .. } => (center, Vector2::default()),
};
let solid_color = match cmd.brush {
Brush::Solid(color) => color,
_ => Color::WHITE,
};
let gradient_colors = match cmd.brush {
Brush::Solid(_) => &raw_colors,
Brush::LinearGradient { ref stops, .. }
| Brush::RadialGradient { ref stops, .. } => {
for (i, point) in stops.iter().enumerate() {
raw_colors[i] = point.color.as_frgba();
}
&raw_colors
}
};
let gradient_stops = match cmd.brush {
Brush::Solid(_) => &raw_stops,
Brush::LinearGradient { ref stops, .. }
| Brush::RadialGradient { ref stops, .. } => {
for (i, point) in stops.iter().enumerate() {
raw_stops[i] = point.stop;
}
&raw_stops
}
};
let brush_type = match cmd.brush {
Brush::Solid(_) => 0,
Brush::LinearGradient { .. } => 1,
Brush::RadialGradient { .. } => 2,
};
let gradient_point_count = match cmd.brush {
Brush::Solid(_) => 0,
Brush::LinearGradient { ref stops, .. }
| Brush::RadialGradient { ref stops, .. } => stops.len() as i32,
};
let is_font_texture = matches!(cmd.texture, CommandTexture::Font { .. });
let buffer = StaticUniformBuffer::<2048>::new()
.with(ortho)
.with(&cmd.transform)
.with(&solid_color)
.with(gradient_colors.as_slice())
.with(gradient_stops.as_slice())
.with(&gradient_origin)
.with(&gradient_end)
.with(&resolution)
.with(&cmd.bounds.position)
.with(&bounds_max)
.with(&is_font_texture)
.with(&cmd.opacity)
.with(&brush_type)
.with(&gradient_point_count);
command_block_locations
.push((resource.binding, uniform_memory_allocator.allocate(buffer)));
} else if let ShaderResourceKind::PropertyGroup(ref shader_property_group) =
resource.kind
{
let mut buf = StaticUniformBuffer::<16384>::new();
if let Some(material_property_group) =
material.property_group_ref(resource.name.clone())
{
bundle::write_with_material(
shader_property_group,
material_property_group,
|c, n| c.property_ref(n.clone()).map(|p| p.as_ref()),
&mut buf,
);
} else {
bundle::write_shader_values(shader_property_group, &mut buf)
}
command_block_locations
.push((resource.binding, uniform_memory_allocator.allocate(buf)));
}
}
block_locations.push(command_block_locations);
}
block_locations
}
impl UiRenderer {
pub(in crate::renderer) fn new(server: &dyn GraphicsServer) -> Result<Self, FrameworkError> {
let geometry_buffer_desc = GpuGeometryBufferDescriptor {
name: "UiGeometryBuffer",
elements: ElementsDescriptor::Triangles(&[]),
buffers: &[VertexBufferDescriptor {
usage: BufferUsage::DynamicDraw,
attributes: &[
AttributeDefinition {
location: 0,
kind: AttributeKind::Float,
component_count: 2,
normalized: false,
divisor: 0,
},
AttributeDefinition {
location: 1,
kind: AttributeKind::Float,
component_count: 2,
normalized: false,
divisor: 0,
},
AttributeDefinition {
location: 2,
kind: AttributeKind::UnsignedByte,
component_count: 4,
normalized: true, // Make sure [0; 255] -> [0; 1]
divisor: 0,
},
],
data: VertexBufferData::new::<crate::gui::draw::Vertex>(None),
}],
usage: BufferUsage::DynamicDraw,
};
let clipping_geometry_buffer_desc = GpuGeometryBufferDescriptor {
name: "UiClippingGeometryBuffer",
elements: ElementsDescriptor::Triangles(&[]),
buffers: &[VertexBufferDescriptor {
usage: BufferUsage::DynamicDraw,
attributes: &[
// We're interested only in position. Fragment shader won't run for clipping geometry anyway.
AttributeDefinition {
location: 0,
kind: AttributeKind::Float,
component_count: 2,
normalized: false,
divisor: 0,
},
],
data: VertexBufferData::new::<crate::gui::draw::Vertex>(None),
}],
usage: BufferUsage::DynamicDraw,
};
Ok(Self {
geometry_buffer: server.create_geometry_buffer(geometry_buffer_desc)?,
clipping_geometry_buffer: server
.create_geometry_buffer(clipping_geometry_buffer_desc)?,
})
}
/// Renders given UI's drawing context to specified frame buffer.
pub fn render(
&mut self,
args: UiRenderContext,
) -> Result<RenderPassStatistics, FrameworkError> {
let UiRenderContext {
server,
viewport,
frame_buffer,
frame_width,
frame_height,
drawing_context,
renderer_resources,
texture_cache,
uniform_buffer_cache,
render_pass_cache,
uniform_memory_allocator,
resource_manager,
} = args;
let mut statistics = RenderPassStatistics::default();
self.geometry_buffer
.set_buffer_data_of_type(0, drawing_context.get_vertices());
self.geometry_buffer
.set_triangles(drawing_context.get_triangles());
let ortho = Matrix4::new_orthographic(0.0, frame_width, frame_height, 0.0, -1.0, 1.0);
let resolution = Vector2::new(frame_width, frame_height);
let uniform_blocks = write_uniform_blocks(
&ortho,
resolution,
drawing_context.get_commands(),
uniform_memory_allocator,
);
uniform_memory_allocator.upload(server)?;
for (cmd, command_uniform_blocks) in
drawing_context.get_commands().iter().zip(uniform_blocks)
{
let mut clip_bounds = cmd.clip_bounds;
clip_bounds.position.x = clip_bounds.position.x.floor();
clip_bounds.position.y = clip_bounds.position.y.floor();
clip_bounds.size.x = clip_bounds.size.x.ceil();
clip_bounds.size.y = clip_bounds.size.y.ceil();
let scissor_box = Some(ScissorBox {
x: clip_bounds.position.x as i32,
// Because OpenGL was designed for mathematicians, it has origin at lower left corner.
y: viewport.size.y - (clip_bounds.position.y + clip_bounds.size.y) as i32,
width: clip_bounds.size.x as i32,
height: clip_bounds.size.y as i32,
});
let mut stencil_test = None;
// Draw clipping geometry first if we have any. This is optional, because complex
// clipping is very rare and in most cases scissor test will do the job.
if let Some(clipping_geometry) = cmd.clipping_geometry.as_ref() {
frame_buffer.clear(viewport, None, None, Some(0));
self.clipping_geometry_buffer
.set_buffer_data_of_type(0, &clipping_geometry.vertex_buffer);
self.clipping_geometry_buffer
.set_triangles(&clipping_geometry.triangle_buffer);
// Draw
let properties = PropertyGroup::from([
property("projectionMatrix", &ortho),
property("worldMatrix", &cmd.transform),
]);
let material = RenderMaterial::from([binding("properties", &properties)]);
statistics += renderer_resources.shaders.ui.run_pass(
1,
&ImmutableString::new("Clip"),
frame_buffer,
&self.geometry_buffer,
viewport,
&material,
uniform_buffer_cache,
Default::default(),
None,
)?;
// Make sure main geometry will be drawn only on marked pixels.
stencil_test = Some(StencilFunc {
func: CompareFunc::Equal,
ref_value: 1,
..Default::default()
});
}
let params = DrawParameters {
cull_face: None,
color_write: ColorMask::all(true),
depth_write: false,
stencil_test,
depth_test: None,
blend: Some(BlendParameters {
func: BlendFunc::new(BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha),
..Default::default()
}),
stencil_op: Default::default(),
scissor_box,
};
let element_range = ElementRange::Specific {
offset: cmd.triangles.start,
count: cmd.triangles.end - cmd.triangles.start,
};
let material_data_guard = cmd.material.data_ref();
let material = some_or_continue!(material_data_guard.as_loaded_ref());
if let Some(render_pass_container) = render_pass_cache.get(server, material.shader()) {
let shader_data_guard = material.shader().data_ref();
let shader = some_or_continue!(shader_data_guard.as_loaded_ref());
let render_pass = render_pass_container.get(&ImmutableString::new("Forward"))?;
let mut resource_bindings = ArrayVec::<ResourceBinding, 32>::new();
for resource in shader.definition.resources.iter() {
match resource.kind {
ShaderResourceKind::Texture { fallback, .. } => {
if resource.name.as_str() == "fyrox_widgetTexture" {
let mut diffuse_texture = (
&renderer_resources.white_dummy,
&renderer_resources.linear_wrap_sampler,
);
match &cmd.texture {
CommandTexture::Font {
font,
page_index,
height,
} => {
if let Some(font) = font.state().data() {
let page_size = font.page_size() as u32;
if let Some(page) = font
.atlases
.get_mut(height)
.and_then(|atlas| atlas.pages.get_mut(*page_index))
{
if page.texture.is_none() || page.modified {
if let Some(details) = Texture::from_bytes(
TextureKind::Rectangle {
width: page_size,
height: page_size,
},
TexturePixelKind::R8,
page.pixels.clone(),
) {
page.texture = Some(
TextureResource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
details,
)
.into(),
);
page.modified = false;
}
}
if let Some(texture) = texture_cache.get(
server,
resource_manager,
&page
.texture
.as_ref()
.unwrap()
.try_cast::<Texture>()
.unwrap(),
) {
diffuse_texture = (
&texture.gpu_texture,
&texture.gpu_sampler,
);
}
}
}
}
CommandTexture::Texture(texture) => {
if let Some(texture) =
texture_cache.get(server, resource_manager, texture)
{
diffuse_texture =
(&texture.gpu_texture, &texture.gpu_sampler);
}
}
_ => (),
}
resource_bindings.push(ResourceBinding::texture(
diffuse_texture.0,
diffuse_texture.1,
resource.binding,
))
} else {
resource_bindings.push(make_texture_binding(
server,
material,
resource,
renderer_resources,
fallback,
resource_manager,
texture_cache,
))
}
}
ShaderResourceKind::PropertyGroup(_) => {
if let Some((_, block_location)) = command_uniform_blocks
.iter()
.find(|(binding, _)| *binding == resource.binding)
{
resource_bindings.push(
uniform_memory_allocator
.block_to_binding(*block_location, resource.binding),
);
}
}
}
}
statistics += frame_buffer.draw(
&self.geometry_buffer,
viewport,
&render_pass.program,
&params,
&[ResourceBindGroup {
bindings: &resource_bindings,
}],
element_range,
)?;
}
}
Ok(statistics)
}
}
+261
View File
@@ -0,0 +1,261 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(missing_docs)] // TODO
use crate::{
core::{
algebra::{Vector2, Vector3},
array_as_u8_slice,
},
graphics::{
error::FrameworkError,
gpu_texture::{CubeMapFace, GpuTexture, GpuTextureDescriptor, GpuTextureKind, PixelKind},
server::GraphicsServer,
},
};
use bytemuck::{Pod, Zeroable};
use half::f16;
use std::{fs::File, io::Write, path::Path};
pub struct CubeMapFaceDescriptor {
pub face: CubeMapFace,
pub look: Vector3<f32>,
pub up: Vector3<f32>,
}
impl CubeMapFaceDescriptor {
pub fn cube_faces() -> [Self; 6] {
[
CubeMapFaceDescriptor {
face: CubeMapFace::PositiveX,
look: Vector3::new(1.0, 0.0, 0.0),
up: Vector3::new(0.0, -1.0, 0.0),
},
CubeMapFaceDescriptor {
face: CubeMapFace::NegativeX,
look: Vector3::new(-1.0, 0.0, 0.0),
up: Vector3::new(0.0, -1.0, 0.0),
},
CubeMapFaceDescriptor {
face: CubeMapFace::PositiveY,
look: Vector3::new(0.0, 1.0, 0.0),
up: Vector3::new(0.0, 0.0, 1.0),
},
CubeMapFaceDescriptor {
face: CubeMapFace::NegativeY,
look: Vector3::new(0.0, -1.0, 0.0),
up: Vector3::new(0.0, 0.0, -1.0),
},
CubeMapFaceDescriptor {
face: CubeMapFace::PositiveZ,
look: Vector3::new(0.0, 0.0, 1.0),
up: Vector3::new(0.0, -1.0, 0.0),
},
CubeMapFaceDescriptor {
face: CubeMapFace::NegativeZ,
look: Vector3::new(0.0, 0.0, -1.0),
up: Vector3::new(0.0, -1.0, 0.0),
},
]
}
}
fn radical_inverse_vd_c(mut bits: u32) -> f32 {
bits = bits.rotate_right(16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
bits as f32 * 2.328_306_4e-10
}
fn hammersley(i: usize, n: usize) -> Vector2<f32> {
Vector2::new(i as f32 / n as f32, radical_inverse_vd_c(i as u32))
}
fn importance_sample_ggx(x_i: Vector2<f32>, roughness: f32, n: Vector3<f32>) -> Vector3<f32> {
let a = roughness * roughness;
let phi = 2.0 * std::f32::consts::PI * x_i.x;
let cos_theta = ((1.0 - x_i.y) / (1.0 + (a * a - 1.0) * x_i.y)).sqrt();
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
// from spherical coordinates to cartesian coordinates
let h = Vector3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta);
// from tangent-space vector to world-space sample vector
let up = if n.z.abs() < 0.999 {
Vector3::new(0.0, 0.0, 1.0)
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let tangent = up.cross(&n).normalize();
let bitangent = n.cross(&tangent);
(tangent * h.x + bitangent * h.y + n * h.z).normalize()
}
fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 {
let a = roughness;
let k = (a * a) / 2.0;
let nom = n_dot_v;
let denom = n_dot_v * (1.0 - k) + k;
nom / denom
}
fn geometry_smith(roughness: f32, n_dot_v: f32, n_dot_l: f32) -> f32 {
let ggx2 = geometry_schlick_ggx(n_dot_v, roughness);
let ggx1 = geometry_schlick_ggx(n_dot_l, roughness);
ggx1 * ggx2
}
fn integrate_brdf(n_dot_v: f32, roughness: f32, samples: usize) -> Vector2<f32> {
let v = Vector3::new((1.0 - n_dot_v * n_dot_v).sqrt(), 0.0, n_dot_v);
let mut a = 0.0;
let mut b = 0.0;
let n = Vector3::new(0.0, 0.0, 1.0);
for i in 0..samples {
let x_i = hammersley(i, samples);
let h = importance_sample_ggx(x_i, roughness, n);
let l = (2.0 * v.dot(&h) * h - v).normalize();
let n_dot_l = l.z.max(0.0);
let n_dot_h = h.z.max(0.0);
let v_dot_h = v.dot(&h).max(0.0);
let n_dot_v = n.dot(&v).max(0.0);
if n_dot_l > 0.0 {
let g = geometry_smith(roughness, n_dot_v, n_dot_l);
let g_vis = (g * v_dot_h) / (n_dot_h * n_dot_v);
let fc = (1.0 - v_dot_h).powf(5.0);
a += (1.0 - fc) * g_vis;
b += fc * g_vis;
}
}
Vector2::new(a / samples as f32, b / samples as f32)
}
#[derive(Default, Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct Pixel {
pub x: f16,
pub y: f16,
}
pub fn make_brdf_lut_image(size: usize, sample_count: usize) -> Vec<Pixel> {
let mut pixels = vec![Pixel::default(); size * size];
for y in 0..size {
for x in 0..size {
let n_dot_v = (y as f32 + 0.5) * (1.0 / size as f32);
let roughness = (x as f32 + 0.5) * (1.0 / size as f32);
let pair = integrate_brdf(n_dot_v, roughness, sample_count);
let pixel = &mut pixels[y * size + x];
pixel.x = f16::from_f32(pair.x);
pixel.y = f16::from_f32(pair.y);
}
}
pixels
}
pub fn generate_brdf_lut_texture(
server: &dyn GraphicsServer,
size: usize,
sample_count: usize,
) -> Result<GpuTexture, FrameworkError> {
let pixels = make_brdf_lut_image(size, sample_count);
make_brdf_lut(server, size, array_as_u8_slice(&pixels))
}
pub fn make_brdf_lut(
server: &dyn GraphicsServer,
size: usize,
pixels: &[u8],
) -> Result<GpuTexture, FrameworkError> {
server.create_texture(GpuTextureDescriptor {
name: "BrdfLut",
kind: GpuTextureKind::Rectangle {
width: size,
height: size,
},
pixel_kind: PixelKind::RG16F,
mip_count: 1,
data: Some(pixels),
..Default::default()
})
}
pub fn write_brdf_lut(path: &Path, size: usize, sample_count: usize) -> std::io::Result<()> {
let pixels = make_brdf_lut_image(size, sample_count);
let mut file = File::create(path)?;
file.write_all(array_as_u8_slice(&pixels))?;
Ok(())
}
#[repr(C)] // guarantee 'bytes' comes after '_align'
pub(crate) struct AlignedAs<Align, Bytes: ?Sized> {
pub _align: [Align; 0],
pub bytes: Bytes,
}
// https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049/2
#[macro_export]
macro_rules! include_bytes_align_as {
($align_ty:ty, $path:literal) => {{
// const block expression to encapsulate the static
use $crate::renderer::utils::AlignedAs;
// this assignment is made possible by CoerceUnsized
static ALIGNED: &AlignedAs<$align_ty, [u8]> = &AlignedAs {
_align: [],
bytes: *include_bytes!($path),
};
&ALIGNED.bytes
}};
}
#[cfg(test)]
mod test {
use crate::renderer::utils::write_brdf_lut;
use std::path::Path;
// Use this test to write BRDF use by the lighting module.
#[test]
fn test_write_brdf_lut() {
write_brdf_lut(
Path::new("src/renderer/brdf_256x256_256samples.bin"),
256,
256,
)
.unwrap();
}
}
+296
View File
@@ -0,0 +1,296 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Volumetric visibility cache based on occlusion query.
use crate::{
core::{algebra::Vector3, pool::Handle},
graph::SceneGraph,
graphics::{
error::FrameworkError,
query::{GpuQuery, QueryKind, QueryResult},
server::GraphicsServer,
},
scene::{graph::Graph, node::Node},
};
use fxhash::FxHashMap;
use std::fmt::{Debug, Formatter};
struct PendingQuery {
query: GpuQuery,
observer_position: Vector3<f32>,
node: Handle<Node>,
}
impl Debug for PendingQuery {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "pos: {}, node: {}", self.observer_position, self.node)
}
}
#[derive(Debug)]
enum Visibility {
Undefined,
Invisible,
Visible,
}
type NodeVisibilityMap = FxHashMap<Handle<Node>, Visibility>;
/// Volumetric visibility cache based on occlusion query.
#[derive(Debug)]
pub struct ObserverVisibilityCache {
cells: FxHashMap<Vector3<i32>, NodeVisibilityMap>,
pending_queries: Vec<PendingQuery>,
granularity: Vector3<u32>,
distance_discard_threshold: f32,
}
fn world_to_grid(world_position: Vector3<f32>, granularity: Vector3<u32>) -> Vector3<i32> {
Vector3::new(
(world_position.x * (granularity.x as f32)).round() as i32,
(world_position.y * (granularity.y as f32)).round() as i32,
(world_position.z * (granularity.z as f32)).round() as i32,
)
}
fn grid_to_world(grid_position: Vector3<i32>, granularity: Vector3<u32>) -> Vector3<f32> {
Vector3::new(
grid_position.x as f32 / (granularity.x as f32),
grid_position.y as f32 / (granularity.y as f32),
grid_position.z as f32 / (granularity.z as f32),
)
}
impl ObserverVisibilityCache {
/// Creates new visibility cache with the given granularity and distance discard threshold.
/// Granularity in means how much the cache should subdivide the world. For example 2 means that
/// 1 meter cell will be split into 8 blocks by 0.5 meters. Distance discard threshold means how
/// far an observer can without discarding visibility info about distant objects.
pub fn new(granularity: Vector3<u32>, distance_discard_threshold: f32) -> Self {
Self {
cells: Default::default(),
pending_queries: Default::default(),
granularity,
distance_discard_threshold,
}
}
/// Transforms the given world-space position into internal grid-space position.
pub fn world_to_grid(&self, world_position: Vector3<f32>) -> Vector3<i32> {
world_to_grid(world_position, self.granularity)
}
/// Transforms the given grid-space position into the world-space position.
pub fn grid_to_world(&self, grid_position: Vector3<i32>) -> Vector3<f32> {
grid_to_world(grid_position, self.granularity)
}
fn visibility_info(
&self,
observer_position: Vector3<f32>,
node: Handle<Node>,
) -> Option<&Visibility> {
let grid_position = self.world_to_grid(observer_position);
self.cells
.get(&grid_position)
.and_then(|cell| cell.get(&node))
}
/// Checks whether the given object needs an occlusion query for the given observer position.
pub fn needs_occlusion_query(
&self,
observer_position: Vector3<f32>,
node: Handle<Node>,
) -> bool {
let Some(visibility) = self.visibility_info(observer_position, node) else {
// There's no data about the visibility, so the occlusion query is needed.
return true;
};
match visibility {
Visibility::Undefined => {
// There's already an occlusion query on GPU.
false
}
Visibility::Invisible => {
// The object could be invisible from one angle at the observer position, but visible
// from another. Since we're using only position of the observer, we cannot be 100%
// sure, that the object is invisible even if a previous query told us so.
true
}
Visibility::Visible => {
// Some pixels of the object is visible from the given observer position, so we don't
// need a new occlusion query.
false
}
}
}
/// Checks whether the object at the given handle is visible from the given observer position.
/// This method returns `true` for non-completed occlusion queries, because occlusion query is
/// async operation.
pub fn is_visible(&self, observer_position: Vector3<f32>, node: Handle<Node>) -> bool {
let Some(visibility_info) = self.visibility_info(observer_position, node) else {
return false;
};
match *visibility_info {
Visibility::Visible
// Undefined visibility is treated like the object is visible, this is needed because
// GPU queries are async, and we must still render the object to prevent popping light.
| Visibility::Undefined => true,
Visibility::Invisible => false,
}
}
/// Begins a new visibility query (using occlusion query) for the object at the given handle from
/// the given observer position.
pub fn begin_query(
&mut self,
server: &dyn GraphicsServer,
observer_position: Vector3<f32>,
node: Handle<Node>,
) -> Result<(), FrameworkError> {
let query = server.create_query()?;
query.begin(QueryKind::AnySamplesPassed);
self.pending_queries.push(PendingQuery {
query,
observer_position,
node,
});
let grid_position = self.world_to_grid(observer_position);
self.cells
.entry(grid_position)
.or_default()
.entry(node)
.or_insert(Visibility::Undefined);
Ok(())
}
/// Ends the last visibility query.
pub fn end_query(&mut self) {
let last_pending_query = self
.pending_queries
.last()
.expect("begin_query/end_query calls mismatch!");
last_pending_query.query.end();
}
/// This method removes info about too distant objects and processes the pending visibility queries.
pub fn update(&mut self, observer_position: Vector3<f32>) {
self.pending_queries.retain_mut(|pending_query| {
if let Some(QueryResult::AnySamplesPassed(query_result)) =
pending_query.query.try_get_result()
{
let grid_position =
world_to_grid(pending_query.observer_position, self.granularity);
let Some(cell) = self.cells.get_mut(&grid_position) else {
return false;
};
let Some(visibility) = cell.get_mut(&pending_query.node) else {
return false;
};
match visibility {
Visibility::Undefined => match query_result {
true => {
*visibility = Visibility::Visible;
}
false => {
*visibility = Visibility::Invisible;
}
},
Visibility::Invisible => {
if query_result {
// Override "invisibility" - if any fragment of an object is visible, then
// it will remain visible forever. This is ok for non-moving objects only.
*visibility = Visibility::Visible;
}
}
Visibility::Visible => {
// Ignore the query result and keep the visibility.
}
}
false
} else {
true
}
});
// Remove visibility info from the cache for distant cells.
self.cells.retain(|grid_position, _| {
let world_position = grid_to_world(*grid_position, self.granularity);
world_position.metric_distance(&observer_position) < self.distance_discard_threshold
});
}
}
#[derive(Debug)]
struct ObserverData {
position: Vector3<f32>,
visibility_cache: ObserverVisibilityCache,
}
/// Visibility cache that caches visibility info for multiple cameras.
#[derive(Default, Debug)]
pub struct VisibilityCache {
observers: FxHashMap<Handle<Node>, ObserverData>,
}
impl VisibilityCache {
/// Gets or adds new storage for the given observer.
pub fn get_or_register(
&mut self,
graph: &Graph,
observer: Handle<Node>,
) -> &mut ObserverVisibilityCache {
&mut self
.observers
.entry(observer)
.or_insert_with(|| ObserverData {
position: graph[observer].global_position(),
visibility_cache: ObserverVisibilityCache::new(Vector3::repeat(2), 100.0),
})
.visibility_cache
}
/// Updates the cache by removing unused data.
pub fn update(&mut self, graph: &Graph) {
self.observers.retain(|observer, data| {
let Ok(observer_ref) = graph.try_get_node(*observer) else {
return false;
};
data.position = observer_ref.global_position();
data.visibility_cache.update(data.position);
true
});
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Curve loader.
use crate::{
asset::{
io::ResourceIo,
loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader},
},
core::uuid::Uuid,
resource::curve::CurveResourceState,
};
use fyrox_core::reflect::Reflect;
use fyrox_resource::state::LoadError;
use std::{path::PathBuf, sync::Arc};
/// Default implementation for curve loading.
pub struct CurveLoader;
impl ResourceLoader for CurveLoader {
fn extensions(&self) -> &[&str] {
&["curve", "crv"]
}
fn is_native_extension(&self, ext: &str) -> bool {
fyrox_core::cmp_strings_case_insensitive(ext, "curve")
|| fyrox_core::cmp_strings_case_insensitive(ext, "crv")
}
fn data_type_uuid(&self) -> Uuid {
<CurveResourceState as Reflect>::type_info().type_uuid
}
fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
Box::pin(async move {
let curve_state = CurveResourceState::from_file(&path, io.as_ref())
.await
.map_err(LoadError::new)?;
Ok(LoaderPayload::new(curve_state))
})
}
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Curve resource holds a [`Curve`]
use crate::{
asset::{io::ResourceIo, Resource, ResourceData},
core::{io::FileError, math::curve::Curve, reflect::prelude::*, visitor::prelude::*},
};
use std::error::Error;
use std::{
fmt::{Display, Formatter},
path::Path,
};
use uuid::uuid;
pub mod loader;
/// An error that may occur during curve resource loading.
#[derive(Debug)]
pub enum CurveResourceError {
/// An i/o error has occurred.
Io(FileError),
/// An error that may occur due to version incompatibilities.
Visit(VisitError),
}
impl Display for CurveResourceError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CurveResourceError::Io(v) => {
write!(f, "A file load error has occurred {v:?}")
}
CurveResourceError::Visit(v) => {
write!(
f,
"An error that may occur due to version incompatibilities. {v:?}"
)
}
}
}
}
impl From<FileError> for CurveResourceError {
fn from(e: FileError) -> Self {
Self::Io(e)
}
}
impl From<VisitError> for CurveResourceError {
fn from(e: VisitError) -> Self {
Self::Visit(e)
}
}
/// State of the [`CurveResource`]
#[derive(Debug, Clone, Visit, Default, PartialEq, Reflect)]
#[reflect(type_uuid = "f28b949f-28a2-4b68-9089-59c234f58b6b")]
pub struct CurveResourceState {
/// Actual curve.
pub curve: Curve,
}
impl ResourceData for CurveResourceState {
fn save(&mut self, _path: &Path) -> Result<(), Box<dyn Error>> {
// TODO: Add saving.
Err("Saving is not supported!".to_string().into())
}
fn can_be_saved(&self) -> bool {
false
}
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
Some(Box::new(self.clone()))
}
}
impl CurveResourceState {
/// Load a curve resource from the specific file path.
pub async fn from_file(path: &Path, io: &dyn ResourceIo) -> Result<Self, CurveResourceError> {
let bytes = io.load_file(path).await?;
let mut visitor = Visitor::load_from_memory(&bytes)?;
let mut curve = Curve::default();
curve.visit("Curve", &mut visitor)?;
Ok(Self { curve })
}
}
/// Type alias for curve resources.
pub type CurveResource = Resource<CurveResourceState>;
@@ -0,0 +1,131 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::core::byteorder::ReadBytesExt;
use crate::{
core::pool::{Handle, Pool},
resource::fbx::{
document::{attribute::FbxAttribute, FbxDocument, FbxNode, FbxNodeContainer},
error::FbxError,
},
};
use std::io::{Read, Seek, SeekFrom};
pub fn read_ascii<R>(reader: &mut R) -> Result<FbxDocument, FbxError>
where
R: Read + Seek,
{
let mut nodes: Pool<FbxNode> = Pool::new();
let root_handle = nodes.spawn(FbxNode {
name: String::from("__ROOT__"),
children: Vec::new(),
parent: Handle::NONE,
attributes: Vec::new(),
});
let mut parent_handle: Handle<FbxNode> = root_handle;
let mut node_handle: Handle<FbxNode> = Handle::NONE;
let mut buffer: Vec<u8> = Vec::new();
let mut name: Vec<u8> = Vec::new();
let mut value: Vec<u8> = Vec::new();
let buf_len = reader.seek(SeekFrom::End(0))?;
reader.rewind()?;
// Read line by line
while reader.stream_position()? < buf_len {
// Read line, trim spaces (but leave spaces in quotes)
buffer.clear();
let mut read_all = false;
while reader.stream_position()? < buf_len {
let symbol = reader.read_u8()?;
if symbol == b'\n' {
break;
} else if symbol == b'"' {
read_all = !read_all;
} else if read_all || !symbol.is_ascii_whitespace() {
buffer.push(symbol);
}
}
// Ignore comments and empty lines
if buffer.is_empty() || buffer[0] == b';' {
continue;
}
// Parse string
let mut read_value = false;
name.clear();
for (i, symbol) in buffer.iter().enumerate() {
let symbol = *symbol;
if i == 0 && (symbol == b'-' || symbol.is_ascii_digit()) {
read_value = true;
}
if symbol == b':' && !read_value {
read_value = true;
let name_copy = String::from_utf8(name.clone())?;
let node = FbxNode {
name: name_copy,
attributes: Vec::new(),
parent: parent_handle,
children: Vec::new(),
};
node_handle = nodes.spawn(node);
name.clear();
let parent = nodes.borrow_mut(parent_handle);
parent.children.push(node_handle);
} else if symbol == b'{' {
// Enter child scope
parent_handle = node_handle;
// Commit attribute if we have one
if !value.is_empty() {
let node = nodes.borrow_mut(node_handle);
let string_value = String::from_utf8(value.clone())?;
let attrib = FbxAttribute::String(string_value);
node.attributes.push(attrib);
value.clear();
}
} else if symbol == b'}' {
// Exit child scope
let parent = nodes.borrow_mut(parent_handle);
parent_handle = parent.parent;
} else if symbol == b',' || (i == buffer.len() - 1) {
// Commit attribute
if symbol != b',' {
value.push(symbol);
}
let node = nodes.borrow_mut(node_handle);
let string_value = String::from_utf8(value.clone())?;
let attrib = FbxAttribute::String(string_value);
node.attributes.push(attrib);
value.clear();
} else if !read_value {
name.push(symbol);
} else {
value.push(symbol);
}
}
}
Ok(FbxDocument {
nodes: FbxNodeContainer { nodes },
root: root_handle,
})
}
@@ -0,0 +1,119 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::fmt::Formatter;
pub enum FbxAttribute {
Double(f64),
Float(f32),
Integer(i32),
Long(i64),
Bool(bool),
String(String), // ASCII Fbx always have every attribute in string form
RawData(Vec<u8>),
}
impl std::fmt::Display for FbxAttribute {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
match self {
FbxAttribute::Double(double) => write!(f, "{double}"),
FbxAttribute::Float(float) => write!(f, "{float}"),
FbxAttribute::Integer(integer) => write!(f, "{integer}"),
FbxAttribute::Long(long) => write!(f, "{long}"),
FbxAttribute::Bool(boolean) => write!(f, "{boolean}"),
FbxAttribute::String(string) => write!(f, "{string}"),
FbxAttribute::RawData(raw) => write!(f, "{raw:?}"),
}
}
}
impl FbxAttribute {
pub fn as_i32(&self) -> Result<i32, String> {
match self {
FbxAttribute::Double(val) => Ok(*val as i32),
FbxAttribute::Float(val) => Ok(*val as i32),
FbxAttribute::Integer(val) => Ok(*val),
FbxAttribute::Long(val) => Ok(*val as i32),
FbxAttribute::Bool(val) => Ok(*val as i32),
FbxAttribute::String(val) => match val.parse::<i32>() {
Ok(i) => Ok(i),
Err(_) => Err(format!("Unable to convert string {val} to i32")),
},
FbxAttribute::RawData(_) => Err("Unable to convert raw data to i32".to_string()),
}
}
pub fn as_i64(&self) -> Result<i64, String> {
match self {
FbxAttribute::Double(val) => Ok(*val as i64),
FbxAttribute::Float(val) => Ok(*val as i64),
FbxAttribute::Integer(val) => Ok(i64::from(*val)),
FbxAttribute::Long(val) => Ok(*val),
FbxAttribute::Bool(val) => Ok(*val as i64),
FbxAttribute::String(val) => match val.parse::<i64>() {
Ok(i) => Ok(i),
Err(_) => Err(format!("Unable to convert string {val} to i64")),
},
FbxAttribute::RawData(_) => Err("Unable to convert raw data to i64".to_string()),
}
}
pub fn as_f64(&self) -> Result<f64, String> {
match self {
FbxAttribute::Double(val) => Ok(*val),
FbxAttribute::Float(val) => Ok(f64::from(*val)),
FbxAttribute::Integer(val) => Ok(f64::from(*val)),
FbxAttribute::Long(val) => Ok(*val as f64),
FbxAttribute::Bool(val) => Ok((*val as i64) as f64),
FbxAttribute::String(val) => match val.parse::<f64>() {
Ok(i) => Ok(i),
Err(_) => Err(format!("Unable to convert string {val} to f64")),
},
FbxAttribute::RawData(_) => Err("Unable to convert raw data to f64".to_string()),
}
}
pub fn as_f32(&self) -> Result<f32, String> {
match self {
FbxAttribute::Double(val) => Ok(*val as f32),
FbxAttribute::Float(val) => Ok(*val),
FbxAttribute::Integer(val) => Ok(*val as f32),
FbxAttribute::Long(val) => Ok(*val as f32),
FbxAttribute::Bool(val) => Ok((*val as i32) as f32),
FbxAttribute::String(val) => match val.parse::<f32>() {
Ok(i) => Ok(i),
Err(_) => Err(format!("Unable to convert string {val} to f32")),
},
FbxAttribute::RawData(_) => Err("Unable to convert raw data to f32".to_string()),
}
}
pub fn as_string(&self) -> String {
match self {
FbxAttribute::Double(val) => val.to_string(),
FbxAttribute::Float(val) => val.to_string(),
FbxAttribute::Integer(val) => val.to_string(),
FbxAttribute::Long(val) => val.to_string(),
FbxAttribute::Bool(val) => val.to_string(),
FbxAttribute::String(val) => val.clone(),
FbxAttribute::RawData(val) => String::from_utf8_lossy(val).to_string(),
}
}
}
@@ -0,0 +1,254 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::core::byteorder::{LittleEndian, ReadBytesExt};
use crate::{
core::pool::{Handle, Pool},
resource::fbx::{
document::{attribute::FbxAttribute, FbxDocument, FbxNode, FbxNodeContainer},
error::FbxError,
},
};
use std::io::{Cursor, Read, Seek, SeekFrom};
fn read_attribute<R>(type_code: u8, file: &mut R) -> Result<FbxAttribute, FbxError>
where
R: Read,
{
match type_code {
b'f' | b'F' => Ok(FbxAttribute::Float(file.read_f32::<LittleEndian>()?)),
b'd' | b'D' => Ok(FbxAttribute::Double(file.read_f64::<LittleEndian>()?)),
b'l' | b'L' => Ok(FbxAttribute::Long(file.read_i64::<LittleEndian>()?)),
b'i' | b'I' => Ok(FbxAttribute::Integer(file.read_i32::<LittleEndian>()?)),
b'Y' => Ok(FbxAttribute::Integer(i32::from(
file.read_i16::<LittleEndian>()?,
))),
b'b' | b'C' => Ok(FbxAttribute::Bool(file.read_u8()? != 0)),
_ => Err(FbxError::UnknownAttributeType(type_code)),
}
}
fn read_array<R>(type_code: u8, file: &mut R) -> Result<Vec<FbxAttribute>, FbxError>
where
R: Read,
{
let length = file.read_u32::<LittleEndian>()? as usize;
let encoding = file.read_u32::<LittleEndian>()?;
let compressed_length = file.read_u32::<LittleEndian>()? as usize;
let mut array = Vec::new();
if encoding == 0 {
for _ in 0..length {
array.push(read_attribute(type_code, file)?);
}
} else {
let mut compressed = vec![Default::default(); compressed_length];
file.read_exact(compressed.as_mut_slice())?;
let decompressed = inflate::inflate_bytes_zlib(&compressed)?;
let mut cursor = Cursor::new(decompressed);
for _ in 0..length {
array.push(read_attribute(type_code, &mut cursor)?);
}
}
Ok(array)
}
fn read_string<R>(file: &mut R) -> Result<FbxAttribute, FbxError>
where
R: Read,
{
let length = file.read_u32::<LittleEndian>()? as usize;
let mut raw_string = vec![Default::default(); length];
file.read_exact(raw_string.as_mut_slice())?;
// Find null terminator. It is required because for some reason some strings
// have additional data after null terminator like this: Omni004\x0\x1Model, but
// length still more than position of null terminator.
if let Some(null_terminator_pos) = raw_string.iter().position(|c| *c == 0) {
raw_string.truncate(null_terminator_pos);
}
let string = String::from_utf8(raw_string)?;
Ok(FbxAttribute::String(string))
}
const VERSION_7500: i32 = 7500;
const VERSION_7500_NULLREC_SIZE: usize = 25; // in bytes
const NORMAL_NULLREC_SIZE: usize = 13; // in bytes
/// Read binary FBX DOM using this specification:
/// https://code.blender.org/2013/08/fbx-binary-file-format-specification/
/// In case of success returns Ok(valid_handle), in case if no more nodes
/// are present returns Ok(none_handle), in case of error returns some FbxError.
fn read_binary_node<R>(
file: &mut R,
pool: &mut Pool<FbxNode>,
version: i32,
) -> Result<Handle<FbxNode>, FbxError>
where
R: Read + Seek,
{
let end_offset = if version < VERSION_7500 {
u64::from(file.read_u32::<LittleEndian>()?)
} else {
file.read_u64::<LittleEndian>()?
};
if end_offset == 0 {
// Footer found. We're done.
return Ok(Handle::NONE);
}
let num_attrib = if version < VERSION_7500 {
file.read_u32::<LittleEndian>()? as usize
} else {
file.read_u64::<LittleEndian>()? as usize
};
let _attrib_list_len = if version < VERSION_7500 {
file.read_u32::<LittleEndian>()? as u64
} else {
file.read_u64::<LittleEndian>()?
};
// Read name.
let name_len = file.read_u8()? as usize;
let mut raw_name = vec![Default::default(); name_len];
file.read_exact(raw_name.as_mut_slice())?;
let node = FbxNode {
name: String::from_utf8(raw_name)?,
..FbxNode::default()
};
let node_handle = pool.spawn(node);
// Read attributes.
for _ in 0..num_attrib {
let type_code = file.read_u8()?;
match type_code {
b'C' | b'Y' | b'I' | b'F' | b'D' | b'L' => {
let node = pool.borrow_mut(node_handle);
node.attributes.push(read_attribute(type_code, file)?);
}
b'f' | b'd' | b'l' | b'i' | b'b' => {
let a = FbxNode {
name: String::from("a"),
attributes: read_array(type_code, file)?,
parent: node_handle,
..FbxNode::default()
};
let a_handle = pool.spawn(a);
let node = pool.borrow_mut(node_handle);
node.children.push(a_handle);
}
b'S' => pool
.borrow_mut(node_handle)
.attributes
.push(read_string(file)?),
b'R' => {
let length = i64::from(file.read_u32::<LittleEndian>()?);
let mut data = vec![0; length as usize];
file.read_exact(&mut data)?;
pool.borrow_mut(node_handle)
.attributes
.push(FbxAttribute::RawData(data));
}
_ => (),
}
}
if file.stream_position()? < end_offset {
let nullrec_size = if version < VERSION_7500 {
NORMAL_NULLREC_SIZE
} else {
VERSION_7500_NULLREC_SIZE
};
let null_record_position = end_offset - nullrec_size as u64;
while file.stream_position()? < null_record_position {
let child_handle = read_binary_node(file, pool, version)?;
if child_handle.is_none() {
return Ok(child_handle);
}
pool.borrow_mut(child_handle).parent = node_handle;
pool.borrow_mut(node_handle).children.push(child_handle);
}
// Check if we have a null-record
if version < VERSION_7500 {
let mut null_record = [0; NORMAL_NULLREC_SIZE];
file.read_exact(&mut null_record)?;
if !null_record.iter().all(|i| *i == 0) {
return Err(FbxError::InvalidNullRecord);
}
} else {
let mut null_record = [0; VERSION_7500_NULLREC_SIZE];
file.read_exact(&mut null_record)?;
if !null_record.iter().all(|i| *i == 0) {
return Err(FbxError::InvalidNullRecord);
}
}
}
Ok(node_handle)
}
pub fn read_binary<R>(file: &mut R) -> Result<FbxDocument, FbxError>
where
R: Read + Seek,
{
let total_length = file.seek(SeekFrom::End(0))?;
file.rewind()?;
// Ignore all stuff until version.
let mut temp = [0; 23];
file.read_exact(&mut temp)?;
// Verify version.
let version = file.read_u32::<LittleEndian>()? as i32;
// Anything else should be supported.
if version < 7100 {
return Err(FbxError::UnsupportedVersion(version));
}
let mut nodes = Pool::new();
let root = FbxNode {
name: String::from("__ROOT__"),
..FbxNode::default()
};
let root_handle = nodes.spawn(root);
// FBX document can have multiple root nodes, so we must read the file
// until the end.
while file.stream_position()? < total_length {
let root_child = read_binary_node(file, &mut nodes, version)?;
if root_child.is_none() {
break;
}
nodes.borrow_mut(root_child).parent = root_handle;
nodes.borrow_mut(root_handle).children.push(root_child);
}
Ok(FbxDocument {
nodes: FbxNodeContainer { nodes },
root: root_handle,
})
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
mod ascii;
pub mod attribute;
mod binary;
use fyrox_resource::io::ResourceIo;
use crate::{
core::{
algebra::Vector3,
pool::{Handle, Pool},
},
resource::fbx::{document::attribute::FbxAttribute, error::FbxError},
};
use std::{io::Cursor, path::Path};
pub struct FbxNode {
name: String,
attributes: Vec<FbxAttribute>,
parent: Handle<FbxNode>,
children: Vec<Handle<FbxNode>>,
}
impl Default for FbxNode {
fn default() -> Self {
Self {
name: "".to_string(),
attributes: Vec::new(),
parent: Default::default(),
children: Vec::new(),
}
}
}
impl FbxNode {
pub fn get_vec3_at(&self, n: usize) -> Result<Vector3<f32>, String> {
Ok(Vector3::new(
self.get_attrib(n)?.as_f32()?,
self.get_attrib(n + 1)?.as_f32()?,
self.get_attrib(n + 2)?.as_f32()?,
))
}
pub fn get_attrib(&self, n: usize) -> Result<&FbxAttribute, String> {
match self.attributes.get(n) {
Some(attrib) => Ok(attrib),
None => Err(format!(
"Unable to get {n} attribute because index out of bounds."
)),
}
}
pub fn attrib_count(&self) -> usize {
self.attributes.len()
}
pub fn attributes(&self) -> &[FbxAttribute] {
&self.attributes
}
pub fn children(&self) -> &[Handle<FbxNode>] {
&self.children
}
pub fn name(&self) -> &str {
&self.name
}
}
pub struct FbxNodeContainer {
nodes: Pool<FbxNode>,
}
impl FbxNodeContainer {
/// Searches node by specified name and returns its handle if found
pub fn find(&self, root: Handle<FbxNode>, name: &str) -> Result<Handle<FbxNode>, String> {
let node = self.nodes.borrow(root);
if node.name == name {
return Ok(root);
}
for child_handle in node.children.iter() {
if let Ok(result) = self.find(*child_handle, name) {
return Ok(result);
}
}
Err(format!("FBX DOM: Unable to find {name} node"))
}
/// Searches node by specified name and borrows a reference to it
pub fn get_by_name(&self, root: Handle<FbxNode>, name: &str) -> Result<&'_ FbxNode, String> {
let node = self.nodes.borrow(root);
if node.name == name {
return Ok(node);
}
for child_handle in node.children.iter() {
if let Ok(result) = self.get_by_name(*child_handle, name) {
return Ok(result);
}
}
Err(format!("FBX DOM: Unable to find {name} node"))
}
pub fn get(&self, handle: Handle<FbxNode>) -> &FbxNode {
self.nodes.borrow(handle)
}
}
pub struct FbxDocument {
root: Handle<FbxNode>,
nodes: FbxNodeContainer,
}
fn is_binary(data: &[u8]) -> bool {
let fbx_magic = b"Kaydara FBX Binary";
&data[0..18] == fbx_magic
}
impl FbxDocument {
pub async fn new<P: AsRef<Path>>(
path: P,
io: &dyn ResourceIo,
) -> Result<FbxDocument, FbxError> {
let data = io.load_file(path.as_ref()).await?;
let is_bin = is_binary(&data);
let mut reader = Cursor::new(data);
if is_bin {
binary::read_binary(&mut reader)
} else {
ascii::read_ascii(&mut reader)
}
}
pub fn root(&self) -> Handle<FbxNode> {
self.root
}
pub fn nodes(&self) -> &FbxNodeContainer {
&self.nodes
}
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Contains all possible errors that can occur during FBX parsing and conversion.
use crate::core::io::FileError;
use std::fmt::{Display, Formatter};
/// See module docs.
#[derive(Debug)]
pub enum FbxError {
/// An input/output error has occurred (unexpected end of file, etc.)
Io(std::io::Error),
/// Type of attribute is unknown or not supported.
UnknownAttributeType(u8),
/// Corrupted null record of binary FBX.
InvalidNullRecord,
/// A string has invalid content (non UTF8-compliant)
InvalidString,
/// Arbitrary error that can have any meaning.
Custom(Box<String>),
/// Version is not supported.
UnsupportedVersion(i32),
/// Internal handle is invalid.
InvalidPoolHandle,
/// Attempt to "cast" enum to unexpected variant.
UnexpectedType,
/// Internal error that means some index was out of bounds. Probably a bug in implementation.
IndexOutOfBounds,
/// Vertex references non existing bone.
UnableToFindBone,
/// There is no corresponding scene node for a FBX model.
UnableToRemapModelToNode,
/// Unknown or unsupported mapping.
InvalidMapping,
/// Unknown or unsupported reference.
InvalidReference,
/// An error occurred during file loading.
FileLoadError(FileError),
}
impl std::error::Error for FbxError {}
impl Display for FbxError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FbxError::Io(v) => {
write!(f, "FBX: Io error: {v}")
}
FbxError::UnknownAttributeType(v) => {
write!(f, "FBX: Unknown or unsupported attribute type {v}")
}
FbxError::InvalidNullRecord => {
write!(f, "FBX: Corrupted null record of binary FBX")
}
FbxError::InvalidString => {
write!(f, "FBX: A string has invalid content (non UTF8-compliant)")
}
FbxError::Custom(v) => {
write!(f, "FBX: An error has occurred: {v}")
}
FbxError::UnsupportedVersion(v) => {
write!(f, "FBX: Version is not supported: {v}")
}
FbxError::InvalidPoolHandle => {
write!(f, "FBX: Internal handle is invalid.")
}
FbxError::UnexpectedType => {
write!(f, "FBX: Internal invalid cast.")
}
FbxError::IndexOutOfBounds => {
write!(f, "FBX: Index is out-of-bounds.")
}
FbxError::UnableToFindBone => {
write!(f, "FBX: Vertex references non existing bone.")
}
FbxError::UnableToRemapModelToNode => {
write!(
f,
"FBX: There is no corresponding scene node for a FBX model."
)
}
FbxError::InvalidMapping => {
write!(f, "FBX: Unknown or unsupported mapping.")
}
FbxError::InvalidReference => {
write!(f, "FBX: Unknown or unsupported reference.")
}
FbxError::FileLoadError(v) => {
write!(f, "FBX: File load error {v:?}.")
}
}
}
}
impl From<FileError> for FbxError {
fn from(err: FileError) -> Self {
FbxError::FileLoadError(err)
}
}
impl From<std::io::Error> for FbxError {
fn from(err: std::io::Error) -> Self {
FbxError::Io(err)
}
}
impl From<String> for FbxError {
fn from(err: String) -> Self {
FbxError::Custom(Box::new(err))
}
}
impl From<std::string::FromUtf8Error> for FbxError {
fn from(_: std::string::FromUtf8Error) -> Self {
FbxError::InvalidString
}
}
+983
View File
@@ -0,0 +1,983 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Contains all methods to load and convert FBX model format.
//!
//! FBX is most flexible format to store and distribute 3D models, it has lots of useful features
//! such as skeletal animation, keyframe animation, support tangents, binormals, materials, etc.
//!
//! Normally you should never use methods from this module directly, use resource manager to load
//! models and create their instances.
mod document;
pub mod error;
mod scene;
use crate::material::MaterialTextureBinding;
use crate::{
asset::manager::ResourceManager,
core::{
algebra::{Matrix4, Point3, UnitQuaternion, Vector2, Vector3, Vector4},
instant::Instant,
log::{Log, MessageKind},
math::curve::{CurveKey, CurveKeyKind},
math::{self, triangulator::triangulate, RotationOrder},
pool::Handle,
},
graph::SceneGraph,
material,
material::MaterialResourceBinding,
resource::{
fbx::{
document::FbxDocument,
error::FbxError,
scene::{
animation::{FbxAnimationCurveNode, FbxAnimationCurveNodeType},
geometry::FbxMeshGeometry,
model::FbxModel,
FbxComponent, FbxMapping, FbxScene,
},
},
model::{MaterialSearchOptions, ModelImportOptions},
texture::{Texture, TextureImportOptions, TextureResource, TextureResourceExtension},
},
scene::{
animation::{Animation, AnimationContainer, AnimationPlayerBuilder, Track},
base::BaseBuilder,
graph::Graph,
mesh::{
buffer::{VertexAttributeUsage, VertexBuffer, VertexWriteTrait},
surface::{
BlendShape, BlendShapesContainer, InputBlendShapeData, Surface, SurfaceData,
SurfaceResource, VertexWeightSet,
},
vertex::{AnimatedVertex, StaticVertex},
Mesh, MeshBuilder,
},
node::Node,
pivot::PivotBuilder,
transform::TransformBuilder,
Scene,
},
utils::{self, raw_mesh::RawMeshBuilder},
};
use fxhash::{FxHashMap, FxHashSet};
use fyrox_animation::track::TrackBinding;
use fyrox_core::err;
use fyrox_material::shader::{ShaderResource, ShaderResourceExtension};
use fyrox_material::MaterialResource;
use fyrox_resource::io::ResourceIo;
use fyrox_resource::untyped::ResourceKind;
use std::{cmp::Ordering, path::Path};
use uuid::Uuid;
/// Input angles in degrees
fn quat_from_euler(euler: Vector3<f32>) -> UnitQuaternion<f32> {
math::quat_from_euler(
Vector3::new(
euler.x.to_radians(),
euler.y.to_radians(),
euler.z.to_radians(),
),
RotationOrder::XYZ,
)
}
/// Fixes index that is used as indicator of end of a polygon
/// FBX stores array of indices like so 0,1,-3,... where -3
/// is actually index 2 but it xor'ed using -1.
fn fix_index(index: i32) -> usize {
if index < 0 {
(index ^ -1) as usize
} else {
index as usize
}
}
/// Triangulates polygon face if needed.
/// Returns number of processed indices.
fn prepare_next_face(
vertices: &[Vector3<f32>],
indices: &[i32],
temp_vertices: &mut Vec<Vector3<f32>>,
out_triangles: &mut Vec<[usize; 3]>,
out_face_triangles: &mut Vec<[usize; 3]>,
) -> usize {
out_triangles.clear();
out_face_triangles.clear();
// Find out how much vertices do we have per face.
let mut vertex_per_face = 0;
for &index in indices {
vertex_per_face += 1;
if index < 0 {
break;
}
}
match vertex_per_face.cmp(&3) {
Ordering::Less => {
// Silently ignore invalid faces.
}
Ordering::Equal => {
let a = fix_index(indices[0]);
let b = fix_index(indices[1]);
let c = fix_index(indices[2]);
// Ensure that we have valid indices here. Some exporters may fuck up indices
// and they'll blow up loader.
if a < vertices.len() && b < vertices.len() && c < vertices.len() {
// We have a triangle
out_triangles.push([a, b, c]);
out_face_triangles.push([0, 1, 2]);
}
}
Ordering::Greater => {
// Found arbitrary polygon, triangulate it.
temp_vertices.clear();
for i in 0..vertex_per_face {
temp_vertices.push(vertices[fix_index(indices[i])]);
}
triangulate(temp_vertices, out_face_triangles);
for triangle in out_face_triangles.iter() {
out_triangles.push([
fix_index(indices[triangle[0]]),
fix_index(indices[triangle[1]]),
fix_index(indices[triangle[2]]),
]);
}
}
}
vertex_per_face
}
#[derive(Clone)]
struct UnpackedVertex {
// Index of surface this vertex belongs to.
surface_index: usize,
position: Vector3<f32>,
normal: Vector3<f32>,
tangent: Vector3<f32>,
uv: Vector2<f32>,
// Set of weights for skinning.
weights: Option<VertexWeightSet>,
}
impl Into<AnimatedVertex> for UnpackedVertex {
fn into(self) -> AnimatedVertex {
AnimatedVertex {
position: self.position,
tex_coord: self.uv,
normal: self.normal,
tangent: Vector4::new(self.tangent.x, self.tangent.y, self.tangent.z, 1.0),
// Correct values will be assigned in second pass of conversion
// when all nodes will be converted.
bone_weights: Default::default(),
bone_indices: Default::default(),
}
}
}
impl Into<StaticVertex> for UnpackedVertex {
fn into(self) -> StaticVertex {
StaticVertex {
position: self.position,
tex_coord: self.uv,
normal: self.normal,
tangent: Vector4::new(self.tangent.x, self.tangent.y, self.tangent.z, 1.0),
}
}
}
fn convert_vertex(
geom: &FbxMeshGeometry,
geometric_transform: &Matrix4<f32>,
material_index: usize,
index: usize,
index_in_polygon: usize,
skin_data: &[VertexWeightSet],
) -> Result<UnpackedVertex, FbxError> {
let position = *geom.vertices.get(index).ok_or(FbxError::IndexOutOfBounds)?;
let normal = match geom.normals.as_ref() {
Some(normals) => *normals.get(index, index_in_polygon)?,
None => Vector3::y(),
};
let tangent = match geom.tangents.as_ref() {
Some(tangents) => *tangents.get(index, index_in_polygon)?,
None => Vector3::y(),
};
let uv = match geom.uvs.as_ref() {
Some(uvs) => *uvs.get(index, index_in_polygon)?,
None => Vector2::default(),
};
let material = match geom.materials.as_ref() {
Some(materials) => *materials.get(material_index, index_in_polygon)?,
None => 0,
};
Ok(UnpackedVertex {
position: geometric_transform
.transform_point(&Point3::from(position))
.coords,
normal: geometric_transform.transform_vector(&normal),
tangent: geometric_transform.transform_vector(&tangent),
uv: Vector2::new(uv.x, 1.0 - uv.y), // Invert Y because OpenGL has origin at left *bottom* corner.
surface_index: material as usize,
weights: if geom.deformers.is_empty() {
None
} else {
Some(*skin_data.get(index).ok_or(FbxError::IndexOutOfBounds)?)
},
})
}
#[derive(Clone)]
enum FbxMeshBuilder {
Static(RawMeshBuilder<StaticVertex>),
Animated(RawMeshBuilder<AnimatedVertex>),
}
impl FbxMeshBuilder {
fn build(self) -> SurfaceData {
match self {
FbxMeshBuilder::Static(builder) => SurfaceData::from_raw_mesh(builder.build()),
FbxMeshBuilder::Animated(builder) => SurfaceData::from_raw_mesh(builder.build()),
}
}
}
#[derive(Clone)]
struct FbxSurfaceData {
base_mesh_builder: FbxMeshBuilder,
blend_shapes: Vec<InputBlendShapeData>,
skin_data: Vec<VertexWeightSet>,
}
fn make_blend_shapes_container(
base_shape: &VertexBuffer,
blend_shapes: Vec<InputBlendShapeData>,
) -> Option<BlendShapesContainer> {
if blend_shapes.is_empty() {
None
} else {
Some(BlendShapesContainer::from_lists(base_shape, &blend_shapes))
}
}
type EngineMaterial = material::Material;
type MaterialMap = FxHashMap<Handle<FbxComponent>, MaterialResource>;
async fn create_materials(
fbx_scene: &FbxScene,
resource_manager: &ResourceManager,
model_import_options: &ModelImportOptions,
model_path: &Path,
) -> Result<MaterialMap, FbxError> {
let mut map = MaterialMap::default();
for (component_handle, component) in fbx_scene.pair_iter() {
if let FbxComponent::Material(fbx_material) = component {
let mut material = EngineMaterial::from_shader(ShaderResource::standard());
material.set_property("diffuseColor", fbx_material.diffuse_color);
let io = resource_manager.resource_io();
for (name, texture_handle) in fbx_material.textures.iter() {
let texture = fbx_scene.get(*texture_handle).as_texture()?;
let path = texture.get_root_file_path(&fbx_scene.components);
if let Some(filename) = path.file_name() {
let texture_path = if texture.content.is_empty() {
match model_import_options.material_search_options {
MaterialSearchOptions::MaterialsDirectory(ref directory) => {
Some(directory.join(filename))
}
MaterialSearchOptions::RecursiveUp => {
let mut texture_path = None;
let mut path = model_path.to_owned();
while let Some(parent) = path.parent() {
let candidate = parent.join(filename);
if io.exists(&candidate).await {
texture_path = Some(candidate);
break;
}
path.pop();
}
texture_path
}
MaterialSearchOptions::WorkingDirectory => {
let mut texture_path = None;
let path = Path::new(".");
if let Ok(iter) = io.walk_directory(path, usize::MAX).await {
for dir in iter {
if io.is_dir(&dir).await {
let candidate = dir.join(filename);
if candidate.exists() {
texture_path = Some(candidate);
break;
}
}
}
}
texture_path
}
MaterialSearchOptions::UsePathDirectly => Some(path.clone()),
}
} else {
Some(path.clone())
};
if let Some(texture_path) = texture_path {
let texture = if texture.content.is_empty() {
resource_manager.request::<Texture>(texture_path.as_path())
} else {
TextureResource::load_from_memory(
Uuid::new_v4(),
ResourceKind::External,
&texture.content,
TextureImportOptions::default(),
)
.unwrap()
};
// Make up your mind, Autodesk and Blender.
// Handle all possible combinations of links to auto-import materials.
let name = if name.contains("AmbientColor")
|| name.contains("ambient_color")
{
Some("aoTexture")
} else if name.contains("DiffuseColor")
|| name.contains("diffuse_color")
|| name.contains("base_color_map")
|| name.contains("texmap_diffuse")
{
Some("diffuseTexture")
} else if name.contains("MetalnessMap")
|| name.contains("metalness_map")
|| name.contains("ReflectionFactor")
|| name.contains("texmap_reflection")
|| name.contains("texmap_metalness")
{
Some("metallicTexture")
} else if name.contains("RoughnessMap")
|| name.contains("roughness_map")
|| name.contains("Shininess")
|| name.contains("ShininessExponent")
|| name.contains("texmap_roughness")
{
Some("roughnessTexture")
} else if name.contains("Bump")
|| name.contains("bump_map")
|| name.contains("NormalMap")
|| name.contains("normal_map")
|| name.contains("texmap_bump")
{
Some("normalTexture")
} else if name.contains("DisplacementColor")
|| name.contains("displacement_map")
{
Some("heightTexture")
} else if name.contains("EmissiveColor") || name.contains("emit_color_map")
{
Some("emissionTexture")
} else {
None
};
if let Some(property_name) = name {
material.bind(
property_name,
MaterialResourceBinding::Texture(MaterialTextureBinding {
value: Some(texture),
}),
);
}
} else {
Log::writeln(
MessageKind::Warning,
format!(
"Unable to find a texture {filename:?} for 3D model {model_path:?} using {model_import_options:?} option!"
),
);
}
}
}
let old_material =
map.insert(component_handle, MaterialResource::new_embedded(material));
assert!(old_material.is_none());
}
}
Ok(map)
}
fn create_surfaces(
data_set: Vec<FbxSurfaceData>,
model: &FbxModel,
materials: &MaterialMap,
) -> Result<Vec<Surface>, FbxError> {
let mut surfaces = Vec::new();
// Create surfaces per material
if model.materials.is_empty() {
assert_eq!(data_set.len(), 1);
let data = data_set.into_iter().next().unwrap();
let mut surface_data = data.base_mesh_builder.build();
surface_data.blend_shapes_container =
make_blend_shapes_container(&surface_data.vertex_buffer, data.blend_shapes);
let mut surface = Surface::new(SurfaceResource::new_ok(
Uuid::new_v4(),
ResourceKind::External,
surface_data,
));
surface.vertex_weights = data.skin_data;
surfaces.push(surface);
} else {
assert_eq!(data_set.len(), model.materials.len());
for (&material_handle, data) in model.materials.iter().zip(data_set) {
let mut surface_data = data.base_mesh_builder.build();
surface_data.blend_shapes_container =
make_blend_shapes_container(&surface_data.vertex_buffer, data.blend_shapes);
let mut surface = Surface::new(SurfaceResource::new_ok(
Uuid::new_v4(),
ResourceKind::External,
surface_data,
));
surface.vertex_weights = data.skin_data;
if let Some(material) = materials.get(&material_handle) {
surface.set_material(material.clone());
} else {
err!("No respective material");
}
surfaces.push(surface);
}
}
Ok(surfaces)
}
fn convert_mesh(
base: BaseBuilder,
fbx_scene: &FbxScene,
model: &FbxModel,
graph: &mut Graph,
materials: &MaterialMap,
) -> Result<Handle<Mesh>, FbxError> {
let geometric_transform = Matrix4::new_translation(&model.geometric_translation)
* quat_from_euler(model.geometric_rotation).to_homogeneous()
* Matrix4::new_nonuniform_scaling(&model.geometric_scale);
let mut temp_vertices = Vec::new();
let mut triangles = Vec::new();
// Array for triangulation needs, it will contain triangle definitions for
// triangulated polygon.
let mut face_triangles = Vec::new();
let mut mesh_surfaces = Vec::new();
let mut mesh_blend_shapes = Vec::new();
for &geom_handle in &model.geoms {
let geom = fbx_scene.get(geom_handle).as_mesh_geometry()?;
let skin_data = geom.get_skin_data(fbx_scene)?;
let blend_shapes = geom.collect_blend_shapes_refs(fbx_scene)?;
if !mesh_blend_shapes.is_empty() {
Log::warn("More than two geoms with blend shapes?");
}
mesh_blend_shapes = blend_shapes
.iter()
.map(|bs| BlendShape {
weight: bs.deform_percent,
name: bs.name.clone(),
})
.collect();
let mut data_set = vec![
FbxSurfaceData {
base_mesh_builder: if geom.deformers.is_empty() {
FbxMeshBuilder::Static(RawMeshBuilder::new(1024, 1024))
} else {
FbxMeshBuilder::Animated(RawMeshBuilder::new(1024, 1024))
},
blend_shapes: blend_shapes
.iter()
.map(|bs_channel| {
InputBlendShapeData {
name: bs_channel.name.clone(),
default_weight: bs_channel.deform_percent,
positions: Default::default(),
normals: Default::default(),
tangents: Default::default(),
}
})
.collect(),
skin_data: Default::default(),
};
model.materials.len().max(1)
];
let mut material_index = 0;
let mut n = 0;
while n < geom.indices.len() {
let origin = n;
n += prepare_next_face(
&geom.vertices,
&geom.indices[origin..],
&mut temp_vertices,
&mut triangles,
&mut face_triangles,
);
for (triangle, face_triangle) in triangles.iter().zip(face_triangles.iter()) {
for (&index, &face_vertex_index) in triangle.iter().zip(face_triangle.iter()) {
let polygon_vertex_index = origin + face_vertex_index;
let vertex = convert_vertex(
geom,
&geometric_transform,
material_index,
index,
polygon_vertex_index,
&skin_data,
)?;
let data = data_set.get_mut(vertex.surface_index).unwrap();
let weights = vertex.weights;
let final_index;
let is_unique_vertex = match data.base_mesh_builder {
FbxMeshBuilder::Static(ref mut builder) => {
final_index = builder.vertex_count();
builder.insert(vertex.clone().into())
}
FbxMeshBuilder::Animated(ref mut builder) => {
final_index = builder.vertex_count();
builder.insert(vertex.clone().into())
}
};
if is_unique_vertex {
if let Some(skin_data) = weights {
data.skin_data.push(skin_data);
}
}
// Fill each blend shape, but modify the vertex first using the "offsets" from blend shapes.
assert_eq!(blend_shapes.len(), data.blend_shapes.len());
for (fbx_blend_shape, blend_shape) in
blend_shapes.iter().zip(data.blend_shapes.iter_mut())
{
let blend_shape_geometry = fbx_scene
.get(fbx_blend_shape.geometry)
.as_shape_geometry()?;
// Only certain vertices are affected by a blend shape, because FBX stores only changed
// parts ("diff").
if let Some(relative_index) =
blend_shape_geometry.indices.get(&(index as i32))
{
blend_shape.positions.insert(
final_index as u32,
utils::vec3_f16_from_f32(
blend_shape_geometry.vertices[*relative_index as usize],
),
);
if let Some(normals) = blend_shape_geometry.normals.as_ref() {
blend_shape.normals.insert(
final_index as u32,
utils::vec3_f16_from_f32(normals[*relative_index as usize]),
);
}
if let Some(tangents) = blend_shape_geometry.tangents.as_ref() {
blend_shape.normals.insert(
final_index as u32,
utils::vec3_f16_from_f32(tangents[*relative_index as usize]),
);
}
}
}
}
}
if let Some(materials) = geom.materials.as_ref() {
if materials.mapping == FbxMapping::ByPolygon {
material_index += 1;
}
}
}
let mut surfaces = create_surfaces(data_set, model, materials)?;
if geom.tangents.is_none() {
for surface in surfaces.iter_mut() {
surface.data().data_ref().calculate_tangents().unwrap();
}
}
for surface in surfaces {
mesh_surfaces.push(surface);
}
}
Ok(MeshBuilder::new(base)
.with_blend_shapes(mesh_blend_shapes)
.with_surfaces(mesh_surfaces)
.build(graph))
}
fn convert_model_to_base(model: &FbxModel) -> BaseBuilder {
BaseBuilder::new()
.with_inv_bind_pose_transform(model.inv_bind_transform)
.with_name(model.name.as_str())
.with_local_transform(
TransformBuilder::new()
.with_local_rotation(quat_from_euler(model.rotation))
.with_local_scale(model.scale)
.with_local_position(model.translation)
.with_post_rotation(quat_from_euler(model.post_rotation))
.with_pre_rotation(quat_from_euler(model.pre_rotation))
.with_rotation_offset(model.rotation_offset)
.with_rotation_pivot(model.rotation_pivot)
.with_scaling_offset(model.scaling_offset)
.with_scaling_pivot(model.scaling_pivot)
.build(),
)
}
fn convert_model(
fbx_scene: &FbxScene,
model: &FbxModel,
graph: &mut Graph,
animation: &mut Animation,
materials: &MaterialMap,
) -> Result<Handle<Node>, FbxError> {
let base = convert_model_to_base(model);
// Create node with the correct kind.
let node_handle = if !model.geoms.is_empty() {
convert_mesh(base, fbx_scene, model, graph, materials)?.to_base()
} else if model.light.is_some() {
fbx_scene.get(model.light).as_light()?.convert(base, graph)
} else {
PivotBuilder::new(base).build(graph).to_base()
};
// Convert animations
if !model.animation_curve_nodes.is_empty() {
// Find supported curve nodes (translation, rotation, scale)
let mut lcl_translation = None;
let mut lcl_rotation = None;
let mut lcl_scale = None;
for &anim_curve_node_handle in model.animation_curve_nodes.iter() {
let component = fbx_scene.get(anim_curve_node_handle);
if let FbxComponent::AnimationCurveNode(curve_node) = component {
if curve_node.actual_type == FbxAnimationCurveNodeType::Rotation {
lcl_rotation = Some(curve_node);
} else if curve_node.actual_type == FbxAnimationCurveNodeType::Translation {
lcl_translation = Some(curve_node);
} else if curve_node.actual_type == FbxAnimationCurveNodeType::Scale {
lcl_scale = Some(curve_node);
}
}
}
fn fill_track<F: Fn(f32) -> f32>(
track: &mut Track,
fbx_scene: &FbxScene,
fbx_track: &FbxAnimationCurveNode,
default: Vector3<f32>,
transform_value: F,
) {
let curves = track.data_container_mut().curves_mut();
if !fbx_track.curves.contains_key("d|X") {
curves[0].add_key(CurveKey::new(0.0, default.x, CurveKeyKind::Constant));
}
if !fbx_track.curves.contains_key("d|Y") {
curves[1].add_key(CurveKey::new(0.0, default.y, CurveKeyKind::Constant));
}
if !fbx_track.curves.contains_key("d|Z") {
curves[2].add_key(CurveKey::new(0.0, default.z, CurveKeyKind::Constant));
}
for (id, curve_handle) in fbx_track.curves.iter() {
let index = match id.as_str() {
"d|X" => Some(0),
"d|Y" => Some(1),
"d|Z" => Some(2),
_ => None,
};
if let Some(index) = index {
if let FbxComponent::AnimationCurve(fbx_curve) = fbx_scene.get(*curve_handle) {
if fbx_curve.keys.is_empty() {
curves[index].add_key(CurveKey::new(
0.0,
default[index],
CurveKeyKind::Constant,
));
} else {
for pair in fbx_curve.keys.iter() {
curves[index].add_key(CurveKey::new(
pair.time,
transform_value(pair.value),
CurveKeyKind::Linear,
))
}
}
}
}
}
}
fn add_vec3_key(track: &mut Track, value: Vector3<f32>) {
let curves = track.data_container_mut().curves_mut();
curves[0].add_key(CurveKey::new(0.0, value.x, CurveKeyKind::Constant));
curves[1].add_key(CurveKey::new(0.0, value.y, CurveKeyKind::Constant));
curves[2].add_key(CurveKey::new(0.0, value.z, CurveKeyKind::Constant));
}
// Convert to engine format
let mut translation_track = Track::new_position();
if let Some(lcl_translation) = lcl_translation {
fill_track(
&mut translation_track,
fbx_scene,
lcl_translation,
model.translation,
|v| v,
);
} else {
add_vec3_key(&mut translation_track, model.translation);
}
let mut rotation_track = Track::new_rotation();
if let Some(lcl_rotation) = lcl_rotation {
fill_track(
&mut rotation_track,
fbx_scene,
lcl_rotation,
model.rotation,
|v| v.to_radians(),
);
} else {
add_vec3_key(&mut rotation_track, model.rotation);
}
let mut scale_track = Track::new_scale();
if let Some(lcl_scale) = lcl_scale {
fill_track(&mut scale_track, fbx_scene, lcl_scale, model.scale, |v| v);
} else {
add_vec3_key(&mut scale_track, model.scale);
}
animation.add_track_with_binding(TrackBinding::new(node_handle), translation_track);
animation.add_track_with_binding(TrackBinding::new(node_handle), rotation_track);
animation.add_track_with_binding(TrackBinding::new(node_handle), scale_track);
}
animation.fit_length_to_content();
Ok(node_handle)
}
///
/// Converts FBX DOM to native engine representation.
///
async fn convert(
fbx_scene: &FbxScene,
resource_manager: ResourceManager,
scene: &mut Scene,
model_path: &Path,
model_import_options: &ModelImportOptions,
) -> Result<(), FbxError> {
let root = scene.graph.get_root();
let mut animation = Animation::default();
animation.set_name("Animation");
let materials = create_materials(
fbx_scene,
&resource_manager,
model_import_options,
model_path,
)
.await?;
let mut fbx_model_to_node_map = FxHashMap::default();
for (component_handle, component) in fbx_scene.pair_iter() {
if let FbxComponent::Model(model) = component {
let node = convert_model(
fbx_scene,
model,
&mut scene.graph,
&mut animation,
&materials,
)?;
scene.graph.link_nodes(node, root);
fbx_model_to_node_map.insert(component_handle, node);
}
}
// Do not create the animation player if there's no animation content.
if !animation.tracks_data().data_ref().tracks().is_empty() {
let mut animations_container = AnimationContainer::new();
animations_container.add(animation);
AnimationPlayerBuilder::new(BaseBuilder::new().with_name("AnimationPlayer"))
.with_animations(animations_container)
.build(&mut scene.graph);
}
// Link according to hierarchy
for (&fbx_model_handle, node_handle) in fbx_model_to_node_map.iter() {
if let FbxComponent::Model(fbx_model) = fbx_scene.get(fbx_model_handle) {
for fbx_child_handle in fbx_model.children.iter() {
if let Some(child_handle) = fbx_model_to_node_map.get(fbx_child_handle) {
scene.graph.link_nodes(*child_handle, *node_handle);
}
}
}
}
scene.graph.update_hierarchical_data();
// Remap handles from fbx model to handles of instantiated nodes
// on each surface of each mesh.
for &handle in fbx_model_to_node_map.values() {
if let Some(mesh) = scene.graph[handle].cast_mut::<Mesh>() {
let mut surface_bones = FxHashSet::default();
for surface in mesh.surfaces_mut() {
for weight_set in surface.vertex_weights.iter_mut() {
for weight in weight_set.iter_mut() {
let fbx_model: Handle<FbxComponent> = weight.effector.into();
let bone_handle = fbx_model_to_node_map
.get(&fbx_model)
.ok_or(FbxError::UnableToRemapModelToNode)?;
surface_bones.insert(*bone_handle);
weight.effector = (*bone_handle).into();
}
}
surface
.bones
.set_value_silent(surface_bones.iter().copied().collect());
let data_rc = surface.data();
let mut data = data_rc.data_ref();
if data.vertex_buffer.vertex_count() as usize == surface.vertex_weights.len() {
let mut vertex_buffer_mut = data.vertex_buffer.modify();
for (mut view, weight_set) in vertex_buffer_mut
.iter_mut()
.zip(surface.vertex_weights.iter())
{
let mut indices = Vector4::default();
let mut weights = Vector4::default();
for (k, weight) in weight_set.iter().enumerate() {
indices[k] = surface
.bones
.iter()
.position(|bone_handle| {
*bone_handle == Handle::<Node>::from(weight.effector)
})
.ok_or(FbxError::UnableToFindBone)?
as u8;
weights[k] = weight.value;
}
view.write_4_f32(VertexAttributeUsage::BoneWeight, weights)
.unwrap();
view.write_4_u8(VertexAttributeUsage::BoneIndices, indices)
.unwrap();
}
}
}
}
}
Ok(())
}
/// Tries to load and convert FBX from given path.
///
/// Normally you should never use this method, use resource manager to load models.
pub async fn load_to_scene<P: AsRef<Path>>(
scene: &mut Scene,
resource_manager: ResourceManager,
io: &dyn ResourceIo,
path: P,
model_import_options: &ModelImportOptions,
) -> Result<(), FbxError> {
let start_time = Instant::now();
Log::writeln(
MessageKind::Information,
format!("Trying to load {:?}", path.as_ref()),
);
let now = Instant::now();
let fbx = FbxDocument::new(path.as_ref(), io).await?;
let parsing_time = now.elapsed().as_millis();
let now = Instant::now();
let fbx_scene = FbxScene::new(&fbx)?;
let dom_prepare_time = now.elapsed().as_millis();
let now = Instant::now();
convert(
&fbx_scene,
resource_manager,
scene,
path.as_ref(),
model_import_options,
)
.await?;
let conversion_time = now.elapsed().as_millis();
Log::writeln(MessageKind::Information,
format!("FBX {:?} loaded in {} ms\n\t- Parsing - {} ms\n\t- DOM Prepare - {} ms\n\t- Conversion - {} ms",
path.as_ref(), start_time.elapsed().as_millis(), parsing_time, dom_prepare_time, conversion_time));
// Check for multiple nodes with same name and throw a warning if any.
// It seems that FBX was designed using ass, not brains. It has no unique **persistent**
// IDs for entities, so the only way to find an entity is to use its name, but FBX also
// allows to have multiple entities with the same name. facepalm.jpg
let mut hash_set = FxHashSet::<String>::default();
for node in scene.graph.linear_iter() {
if hash_set.contains(node.name()) {
Log::writeln(
MessageKind::Error,
format!(
"A node with existing name {} was found during the load of {} resource! \
Do **NOT IGNORE** this message, please fix names in your model, otherwise \
engine won't be able to correctly restore data from your resource!",
node.name(),
path.as_ref().display()
),
);
} else {
hash_set.insert(node.name_owned());
}
}
Ok(())
}
@@ -0,0 +1,97 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::pool::Handle,
resource::fbx::{
document::{FbxNode, FbxNodeContainer},
scene::{FbxComponent, FBX_TIME_UNIT},
},
};
use fxhash::FxHashMap;
pub struct FbxTimeValuePair {
pub time: f32,
pub value: f32,
}
pub struct FbxAnimationCurve {
pub keys: Vec<FbxTimeValuePair>,
}
impl FbxAnimationCurve {
pub(in crate::resource::fbx) fn read(
curve_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Self, String> {
let key_time_handle = nodes.find(curve_handle, "KeyTime")?;
let key_time_array = nodes.get_by_name(key_time_handle, "a")?;
let key_value_handle = nodes.find(curve_handle, "KeyValueFloat")?;
let key_value_array = nodes.get_by_name(key_value_handle, "a")?;
if key_time_array.attrib_count() != key_value_array.attrib_count() {
return Err(String::from(
"FBX: Animation curve contains wrong key data!",
));
}
let mut curve = FbxAnimationCurve { keys: Vec::new() };
for i in 0..key_value_array.attrib_count() {
curve.keys.push(FbxTimeValuePair {
time: ((key_time_array.get_attrib(i)?.as_i64()? as f64) * FBX_TIME_UNIT) as f32,
value: key_value_array.get_attrib(i)?.as_f32()?,
});
}
Ok(curve)
}
}
#[derive(PartialEq, Eq)]
pub enum FbxAnimationCurveNodeType {
Unknown,
Translation,
Rotation,
Scale,
}
pub struct FbxAnimationCurveNode {
pub actual_type: FbxAnimationCurveNodeType,
/// Parameter name to curve mapping, usually it has `d|X`, `d|Y`, `d|Z` as key.
pub curves: FxHashMap<String, Handle<FbxComponent>>,
}
impl FbxAnimationCurveNode {
pub fn read(node_handle: Handle<FbxNode>, nodes: &FbxNodeContainer) -> Result<Self, String> {
let node = nodes.get(node_handle);
Ok(FbxAnimationCurveNode {
actual_type: match node.get_attrib(1)?.as_string().as_str() {
"T" | "AnimCurveNode::T" => FbxAnimationCurveNodeType::Translation,
"R" | "AnimCurveNode::R" => FbxAnimationCurveNodeType::Rotation,
"S" | "AnimCurveNode::S" => FbxAnimationCurveNodeType::Scale,
_ => FbxAnimationCurveNodeType::Unknown,
},
curves: Default::default(),
})
}
}
@@ -0,0 +1,309 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{
algebra::{Vector2, Vector3},
log::Log,
pool::Handle,
},
resource::fbx::{
document::{FbxNode, FbxNodeContainer},
error::FbxError,
scene::{
self, attributes_to_vec3_array, FbxBlendShapeChannel, FbxComponent, FbxLayerElement,
FbxScene,
},
},
scene::mesh::surface::{VertexWeight, VertexWeightSet},
};
use fxhash::FxHashMap;
use std::fmt::Debug;
pub struct FbxMeshGeometry {
// Only vertices and indices are required.
pub vertices: Vec<Vector3<f32>>,
pub indices: Vec<i32>,
// Normals, UVs, etc. are optional.
pub normals: Option<FbxLayerElement<Vector3<f32>>>,
pub uvs: Option<FbxLayerElement<Vector2<f32>>>,
pub materials: Option<FbxLayerElement<i32>>,
pub tangents: Option<FbxLayerElement<Vector3<f32>>>,
#[allow(dead_code)] // TODO: Use binormals.
pub binormals: Option<FbxLayerElement<Vector3<f32>>>,
pub deformers: Vec<Handle<FbxComponent>>,
}
fn read_vertices(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Vec<Vector3<f32>>, FbxError> {
let vertices_node_handle = nodes.find(geom_node_handle, "Vertices")?;
let vertices_array_node = nodes.get_by_name(vertices_node_handle, "a")?;
let mut vertices = Vec::with_capacity(vertices_array_node.attrib_count() / 3);
for vertex in vertices_array_node.attributes().chunks_exact(3) {
vertices.push(Vector3::new(
vertex[0].as_f32()?,
vertex[1].as_f32()?,
vertex[2].as_f32()?,
));
}
Ok(vertices)
}
fn read_indices(
name: &str,
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Vec<i32>, FbxError> {
let indices_node_handle = nodes.find(geom_node_handle, name)?;
let indices_array_node = nodes.get_by_name(indices_node_handle, "a")?;
let mut indices = Vec::with_capacity(indices_array_node.attrib_count());
for index in indices_array_node.attributes() {
indices.push(index.as_i32()?);
}
Ok(indices)
}
fn read_normals(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<FbxLayerElement<Vector3<f32>>>, FbxError> {
if let Ok(layer_element_normal) = nodes.find(geom_node_handle, "LayerElementNormal") {
Ok(Some(scene::make_vec3_container(
nodes,
layer_element_normal,
"Normals",
)?))
} else {
Ok(None)
}
}
fn read_tangents(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<FbxLayerElement<Vector3<f32>>>, FbxError> {
if let Ok(layer_element_tangent) = nodes.find(geom_node_handle, "LayerElementTangent") {
Ok(Some(scene::make_vec3_container(
nodes,
layer_element_tangent,
"Tangents",
)?))
} else {
Ok(None)
}
}
fn read_binormals(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<FbxLayerElement<Vector3<f32>>>, FbxError> {
if let Ok(layer_element_tangent) = nodes.find(geom_node_handle, "LayerElementBinormal") {
Ok(Some(scene::make_vec3_container(
nodes,
layer_element_tangent,
"Binormals",
)?))
} else {
Ok(None)
}
}
fn read_uvs(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<FbxLayerElement<Vector2<f32>>>, FbxError> {
if let Ok(layer_element_uv) = nodes.find(geom_node_handle, "LayerElementUV") {
Ok(Some(FbxLayerElement::new(
nodes,
layer_element_uv,
"UV",
|attributes| {
let mut uvs = Vec::with_capacity(attributes.len() / 2);
for uv in attributes.chunks_exact(2) {
uvs.push(Vector2::new(uv[0].as_f32()?, uv[1].as_f32()?));
}
Ok(uvs)
},
)?))
} else {
Ok(None)
}
}
fn read_materials(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<FbxLayerElement<i32>>, FbxError> {
if let Ok(layer_element_material_node_handle) =
nodes.find(geom_node_handle, "LayerElementMaterial")
{
Ok(Some(FbxLayerElement::new(
nodes,
layer_element_material_node_handle,
"Materials",
|attributes| {
let mut materials = Vec::with_capacity(attributes.len());
for attribute in attributes {
materials.push(attribute.as_i32()?);
}
Ok(materials)
},
)?))
} else {
Ok(None)
}
}
fn warn_missing_element<T, E>(result: Result<T, E>) -> T
where
T: Default,
E: Debug,
{
result.unwrap_or_else(|err| {
Log::warn(format!(
"Unable to read FBX element, fallback to defaults. Reason: {err:?}"
));
T::default()
})
}
impl FbxMeshGeometry {
pub(in crate::resource::fbx) fn read(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Self {
// Apparently, every attribute here could be optional, and thus we shouldn't throw an error
// if it is missing. It just means that this geometry has no surfaces in terms of the engine.
Self {
vertices: warn_missing_element(read_vertices(geom_node_handle, nodes)),
indices: warn_missing_element(read_indices(
"PolygonVertexIndex",
geom_node_handle,
nodes,
)),
normals: warn_missing_element(read_normals(geom_node_handle, nodes)),
uvs: warn_missing_element(read_uvs(geom_node_handle, nodes)),
materials: warn_missing_element(read_materials(geom_node_handle, nodes)),
tangents: warn_missing_element(read_tangents(geom_node_handle, nodes)),
binormals: warn_missing_element(read_binormals(geom_node_handle, nodes)),
deformers: Vec::new(),
}
}
pub(in crate::resource::fbx) fn get_skin_data(
&self,
scene: &FbxScene,
) -> Result<Vec<VertexWeightSet>, FbxError> {
let mut out = vec![VertexWeightSet::default(); self.vertices.len()];
for &deformer_handle in self.deformers.iter() {
for &sub_deformer_handle in scene
.get(deformer_handle)
.as_deformer()?
.sub_deformers
.iter()
{
// We must check for cluster here, because skinned meshes can also have blend shape deformers.
if let FbxComponent::Cluster(cluster) = scene.get(sub_deformer_handle) {
for (index, weight) in cluster.weights.iter() {
let bone_set = out
.get_mut(*index as usize)
.ok_or(FbxError::IndexOutOfBounds)?;
if !bone_set.push(VertexWeight {
value: *weight,
effector: cluster.model.into(),
}) {
// Re-normalize weights if there are more than 4 bones per vertex.
bone_set.normalize();
}
}
}
}
}
Ok(out)
}
pub fn collect_blend_shapes_refs<'a>(
&self,
scene: &'a FbxScene,
) -> Result<Vec<&'a FbxBlendShapeChannel>, FbxError> {
let mut blend_shapes = Vec::new();
for &deformer_handle in &self.deformers {
let deformer = scene.get(deformer_handle).as_deformer()?;
for &sub_deformer in &deformer.sub_deformers {
if let FbxComponent::BlendShapeChannel(channel) = scene.get(sub_deformer) {
blend_shapes.push(channel);
}
}
}
Ok(blend_shapes)
}
}
// According to the docs, shape geometry is similar to mesh geometry, but all containers have "index to direct" mapping
// (plain arrays), so we'll store all data in simple Vecs. Shape geometry is used a source of "delta" data for blend
// shapes, so we'll only use some common data here: vertex positions, normals, tangents and binormals.
pub struct FbxShapeGeometry {
// Only vertices and indices are required.
pub vertices: Vec<Vector3<f32>>,
pub indices: FxHashMap<i32, i32>,
// The rest is optional.
pub normals: Option<Vec<Vector3<f32>>>,
pub tangents: Option<Vec<Vector3<f32>>>,
#[allow(dead_code)] // TODO: Use binormals.
pub binormals: Option<Vec<Vector3<f32>>>,
}
fn read_vec3_plain_array(
name: &str,
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Option<Vec<Vector3<f32>>>, FbxError> {
if let Ok(layer_element_normal) = nodes.find(geom_node_handle, name) {
let array_node = nodes.get_by_name(layer_element_normal, "a")?;
Ok(Some(attributes_to_vec3_array(array_node.attributes())?))
} else {
Ok(None)
}
}
impl FbxShapeGeometry {
pub(in crate::resource::fbx) fn read(
geom_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Self, FbxError> {
Ok(Self {
vertices: read_vec3_plain_array("Vertices", geom_node_handle, nodes)?
.ok_or_else(|| "No vertices element!".to_string())?,
indices: read_indices("Indexes", geom_node_handle, nodes)?
.into_iter()
.enumerate()
.map(|(k, i)| (i, k as i32))
.collect(),
normals: read_vec3_plain_array("Normals", geom_node_handle, nodes)?,
tangents: read_vec3_plain_array("Tangents", geom_node_handle, nodes)?,
binormals: read_vec3_plain_array("Binormals", geom_node_handle, nodes)?,
})
}
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{
color::Color,
log::{Log, MessageKind},
pool::Handle,
},
resource::fbx::document::{FbxNode, FbxNodeContainer},
scene::{
base::BaseBuilder,
graph::Graph,
light::{
directional::DirectionalLightBuilder, point::PointLightBuilder, spot::SpotLightBuilder,
BaseLightBuilder,
},
node::Node,
},
};
pub enum FbxLightType {
Point = 0,
Directional = 1,
Spot = 2,
Area = 3,
Volume = 4,
}
pub struct FbxLight {
actual_type: FbxLightType,
color: Color,
radius: f32,
hotspot_cone_angle: f32,
falloff_cone_angle_delta: f32,
}
impl FbxLight {
pub(in crate::resource::fbx) fn read(
light_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Self, String> {
let mut light = Self {
actual_type: FbxLightType::Point,
color: Color::WHITE,
radius: 10.0,
hotspot_cone_angle: 90.0f32.to_radians(),
falloff_cone_angle_delta: 5.0f32.to_radians(),
};
let props = nodes.get_by_name(light_node_handle, "Properties70")?;
for prop_handle in props.children() {
let prop = nodes.get(*prop_handle);
match prop.get_attrib(0)?.as_string().as_str() {
"DecayStart" => light.radius = prop.get_attrib(4)?.as_f64()? as f32,
"Color" => {
let r = (prop.get_attrib(4)?.as_f64()? * 255.0) as u8;
let g = (prop.get_attrib(5)?.as_f64()? * 255.0) as u8;
let b = (prop.get_attrib(6)?.as_f64()? * 255.0) as u8;
light.color = Color::from_rgba(r, g, b, 255);
}
"HotSpot" => {
light.hotspot_cone_angle = (prop.get_attrib(4)?.as_f64()? as f32).to_radians();
}
"Cone angle" => {
light.falloff_cone_angle_delta = (prop.get_attrib(4)?.as_f64()? as f32)
.to_radians()
- light.hotspot_cone_angle;
}
"LightType" => {
let type_code = prop.get_attrib(4)?.as_i32()?;
light.actual_type = match type_code {
0 => FbxLightType::Point,
1 => FbxLightType::Directional,
2 => FbxLightType::Spot,
3 => FbxLightType::Area,
4 => FbxLightType::Volume,
_ => {
Log::writeln(
MessageKind::Warning,
format!("FBX: Unknown light type {type_code}, fallback to Point!"),
);
FbxLightType::Point
}
};
}
_ => (),
}
}
Ok(light)
}
pub fn convert(&self, base: BaseBuilder, graph: &mut Graph) -> Handle<Node> {
match self.actual_type {
FbxLightType::Point | FbxLightType::Area | FbxLightType::Volume => {
PointLightBuilder::new(
BaseLightBuilder::new(base).with_color(self.color.to_opaque()),
)
.with_radius(self.radius)
.build(graph)
.to_base()
}
FbxLightType::Spot => SpotLightBuilder::new(
BaseLightBuilder::new(base).with_color(self.color.to_opaque()),
)
.with_distance(self.radius)
.with_hotspot_cone_angle(self.hotspot_cone_angle)
.with_falloff_angle_delta(self.falloff_cone_angle_delta)
.build(graph)
.to_base(),
FbxLightType::Directional => DirectionalLightBuilder::new(
BaseLightBuilder::new(base).with_color(self.color.to_opaque()),
)
.build(graph)
.to_base(),
}
}
}
+608
View File
@@ -0,0 +1,608 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::{
algebra::{Matrix4, Vector3},
color::Color,
pool::{Handle, Pool},
},
resource::fbx::{
document::{attribute::FbxAttribute, FbxDocument, FbxNode, FbxNodeContainer},
error::FbxError,
fix_index,
scene::{
animation::{FbxAnimationCurve, FbxAnimationCurveNode},
geometry::{FbxMeshGeometry, FbxShapeGeometry},
light::FbxLight,
model::FbxModel,
texture::FbxTexture,
video::FbxVideo,
},
},
};
use fxhash::FxHashMap;
pub mod animation;
pub mod geometry;
pub mod light;
pub mod model;
pub mod texture;
pub mod video;
pub struct FbxScene {
pub components: Pool<FbxComponent>,
}
impl FbxScene {
/// Parses FBX DOM and filling internal lists to prepare
/// for conversion to engine format
pub fn new(document: &FbxDocument) -> Result<Self, FbxError> {
let mut components = Pool::new();
let mut index_to_component = FxHashMap::default();
let nodes = document.nodes();
// Check version
let header_handle = nodes.find(document.root(), "FBXHeaderExtension")?;
let version = nodes.get_by_name(header_handle, "FBXVersion")?;
let version = version.get_attrib(0)?.as_i32()?;
if version < 7100 {
return Err(FbxError::UnsupportedVersion(version));
}
// Read objects
let objects_node = nodes.get_by_name(document.root(), "Objects")?;
for object_handle in objects_node.children() {
let object = nodes.get(*object_handle);
let index = object.get_attrib(0)?.as_i64()?;
let mut component_handle: Handle<FbxComponent> = Handle::NONE;
match object.name() {
"Geometry" => match object.get_attrib(2)?.as_string().as_str() {
"Mesh" => {
component_handle = components.spawn(FbxComponent::MeshGeometry(Box::new(
FbxMeshGeometry::read(*object_handle, nodes),
)));
}
"Shape" => {
component_handle = components.spawn(FbxComponent::ShapeGeometry(Box::new(
FbxShapeGeometry::read(*object_handle, nodes)?,
)));
}
_ => (),
},
"Model" => {
component_handle = components.spawn(FbxComponent::Model(Box::new(
FbxModel::read(*object_handle, nodes)?,
)));
}
"Material" => {
component_handle = components.spawn(FbxComponent::Material(FbxMaterial::read(
*object_handle,
nodes,
)?));
}
"Texture" => {
component_handle = components.spawn(FbxComponent::Texture(FbxTexture::read(
*object_handle,
nodes,
)?));
}
"Video" if object.get_attrib(2)?.as_string() == "Clip" => {
component_handle = components
.spawn(FbxComponent::Video(FbxVideo::read(*object_handle, nodes)?));
}
"NodeAttribute"
if object.attrib_count() > 2
&& object.get_attrib(2)?.as_string() == "Light" =>
{
component_handle = components
.spawn(FbxComponent::Light(FbxLight::read(*object_handle, nodes)?));
}
"AnimationCurve" => {
component_handle = components.spawn(FbxComponent::AnimationCurve(
FbxAnimationCurve::read(*object_handle, nodes)?,
));
}
"AnimationCurveNode" => {
component_handle = components.spawn(FbxComponent::AnimationCurveNode(
FbxAnimationCurveNode::read(*object_handle, nodes)?,
));
}
"Deformer" => match object.get_attrib(2)?.as_string().as_str() {
"Cluster" => {
component_handle = components.spawn(FbxComponent::Cluster(
FbxCluster::read(*object_handle, nodes)?,
));
}
"BlendShapeChannel" => {
component_handle = components.spawn(FbxComponent::BlendShapeChannel(
FbxBlendShapeChannel::read(*object_handle, nodes)?,
))
}
"Skin" | "BlendShape" => {
component_handle = components.spawn(FbxComponent::Deformer(
FbxDeformer::read(*object_handle, nodes),
));
}
_ => (),
},
_ => (),
}
if !component_handle.is_none() {
index_to_component.insert(index, component_handle);
}
}
// Read connections
let connections_node = nodes.get_by_name(document.root(), "Connections")?;
for connection_handle in connections_node.children() {
let connection = nodes.get(*connection_handle);
let child_index = connection.get_attrib(1)?.as_i64()?;
let parent_index = connection.get_attrib(2)?.as_i64()?;
let property = match connection.get_attrib(3) {
Ok(attrib) => attrib.as_string(),
Err(_) => String::from(""),
};
if let Some(parent_handle) = index_to_component.get(&parent_index) {
if let Some(child_handle) = index_to_component.get(&child_index) {
let (child, parent) =
components.borrow_two_mut((*child_handle, *parent_handle));
link_child_with_parent_component(parent, child, *child_handle, property);
}
}
}
Ok(Self { components })
}
pub fn pair_iter(&self) -> impl Iterator<Item = (Handle<FbxComponent>, &FbxComponent)> {
self.components.pair_iter()
}
pub fn get(&self, handle: Handle<FbxComponent>) -> &FbxComponent {
self.components.borrow(handle)
}
}
fn link_child_with_parent_component(
parent: &mut FbxComponent,
child: &mut FbxComponent,
child_handle: Handle<FbxComponent>,
property: String,
) {
match parent {
// Link model with other components
FbxComponent::Model(model) => match child {
FbxComponent::MeshGeometry(_) => model.geoms.push(child_handle),
FbxComponent::Material(_) => model.materials.push(child_handle),
FbxComponent::AnimationCurveNode(_) => model.animation_curve_nodes.push(child_handle),
FbxComponent::Light(_) => model.light = child_handle,
FbxComponent::Model(_) => model.children.push(child_handle),
_ => (),
},
// Link material with textures
FbxComponent::Material(material) => {
if let FbxComponent::Texture(_) = child {
material.textures.push((property, child_handle));
}
}
FbxComponent::Texture(texture) => {
if let FbxComponent::Texture(_) = child {
texture.ancestor = child_handle;
} else if let FbxComponent::Video(video) = child {
texture.content.clone_from(&video.content);
}
}
// Link animation curve node with animation curve
FbxComponent::AnimationCurveNode(anim_curve_node) => {
if let FbxComponent::AnimationCurve(_) = child {
anim_curve_node.curves.insert(property, child_handle);
}
}
// Link deformer with sub-deformers (skin cluster, blend shape channel)
FbxComponent::Deformer(deformer) => {
if let FbxComponent::Cluster(_) | FbxComponent::BlendShapeChannel(_) = child {
deformer.sub_deformers.push(child_handle);
}
}
// Link geometry with deformers
FbxComponent::MeshGeometry(geometry) => {
if let FbxComponent::Deformer(_) = child {
geometry.deformers.push(child_handle);
}
}
// Link cluster with model (bones)
FbxComponent::Cluster(sub_deformer) => {
if let FbxComponent::Model(model) = child {
sub_deformer.model = child_handle;
model.inv_bind_transform = sub_deformer.transform;
}
}
FbxComponent::BlendShapeChannel(channel) => {
if let FbxComponent::ShapeGeometry(_) = child {
channel.geometry = child_handle;
}
}
// Ignore rest
_ => (),
}
}
pub enum FbxComponent {
Deformer(FbxDeformer),
Cluster(FbxCluster),
BlendShapeChannel(FbxBlendShapeChannel),
Texture(FbxTexture),
Video(FbxVideo),
Light(FbxLight),
Model(Box<FbxModel>),
Material(FbxMaterial),
AnimationCurveNode(FbxAnimationCurveNode),
AnimationCurve(FbxAnimationCurve),
MeshGeometry(Box<FbxMeshGeometry>),
ShapeGeometry(Box<FbxShapeGeometry>),
}
macro_rules! define_as {
($self:ident, $name:ident, $ty:ty, $kind:ident) => {
#[allow(dead_code)]
pub fn $name(&$self) -> Result<&$ty, FbxError> {
if let FbxComponent::$kind(component) = $self {
Ok(component)
} else {
Err(FbxError::UnexpectedType)
}
}
}
}
impl FbxComponent {
define_as!(self, as_deformer, FbxDeformer, Deformer);
define_as!(self, as_texture, FbxTexture, Texture);
define_as!(self, as_light, FbxLight, Light);
define_as!(self, as_material, FbxMaterial, Material);
define_as!(self, as_mesh_geometry, FbxMeshGeometry, MeshGeometry);
define_as!(self, as_shape_geometry, FbxShapeGeometry, ShapeGeometry);
}
// https://help.autodesk.com/view/FBX/2016/ENU/?guid=__cpp_ref_class_fbx_anim_curve_html
const FBX_TIME_UNIT: f64 = 1.0 / 46_186_158_000.0;
pub struct FbxBlendShapeChannel {
pub geometry: Handle<FbxComponent>,
pub deform_percent: f32,
pub name: String,
}
impl FbxBlendShapeChannel {
fn read(channel: Handle<FbxNode>, nodes: &FbxNodeContainer) -> Result<Self, String> {
let deform_percent = nodes
.get_by_name(channel, "DeformPercent")
.and_then(|n| n.get_attrib(0).map(|a| a.as_f32().unwrap_or(100.0)))
.unwrap_or(100.0);
let mut name = nodes.get(channel).get_attrib(1)?.as_string();
if let Some(without_prefix) = name.strip_prefix("SubDeformer::") {
name = without_prefix.to_string();
}
Ok(Self {
geometry: Default::default(),
deform_percent,
name,
})
}
}
pub struct FbxCluster {
model: Handle<FbxComponent>,
weights: Vec<(i32, f32)>,
transform: Matrix4<f32>,
}
impl FbxCluster {
fn read(cluster_handle: Handle<FbxNode>, nodes: &FbxNodeContainer) -> Result<Self, String> {
let transform_handle = nodes.find(cluster_handle, "Transform")?;
let transform_node = nodes.get_by_name(transform_handle, "a")?;
if transform_node.attrib_count() != 16 {
return Err(format!(
"FBX: Wrong transform size! Expect 16, got {}",
transform_node.attrib_count()
));
}
let mut transform = Matrix4::identity();
for i in 0..16 {
transform[i] = transform_node.get_attrib(i)?.as_f64()? as f32;
}
let mut weights = Vec::new();
if let Ok(indices_handle) = nodes.find(cluster_handle, "Indexes") {
let indices_node = nodes.get_by_name(indices_handle, "a")?;
let weights_handle = nodes.find(cluster_handle, "Weights")?;
let weights_node = nodes.get_by_name(weights_handle, "a")?;
if indices_node.attrib_count() != weights_node.attrib_count() {
return Err(String::from(
"invalid cluster, weights count does not match index count",
));
}
weights = Vec::with_capacity(weights_node.attrib_count());
for i in 0..weights_node.attrib_count() {
weights.push((
indices_node.get_attrib(i)?.as_i32()?,
weights_node.get_attrib(i)?.as_f64()? as f32,
));
}
}
Ok(FbxCluster {
model: Handle::NONE,
weights,
transform,
})
}
}
pub struct FbxMaterial {
pub textures: Vec<(String, Handle<FbxComponent>)>,
pub diffuse_color: Color,
}
impl FbxMaterial {
fn read(
material_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<FbxMaterial, FbxError> {
let mut diffuse_color = Color::WHITE;
let props = nodes.get_by_name(material_node_handle, "Properties70")?;
for prop_handle in props.children() {
let prop = nodes.get(*prop_handle);
if let "DiffuseColor" = prop.get_attrib(0)?.as_string().as_str() {
let r = (prop.get_attrib(4)?.as_f64()? * 255.0) as u8;
let g = (prop.get_attrib(5)?.as_f64()? * 255.0) as u8;
let b = (prop.get_attrib(6)?.as_f64()? * 255.0) as u8;
diffuse_color = Color::from_rgba(r, g, b, 255);
}
}
Ok(FbxMaterial {
textures: Default::default(),
diffuse_color,
})
}
}
pub struct FbxDeformer {
pub sub_deformers: Vec<Handle<FbxComponent>>,
}
impl FbxDeformer {
fn read(_sub_deformer_handle: Handle<FbxNode>, _nodes: &FbxNodeContainer) -> Self {
FbxDeformer {
sub_deformers: Vec::new(),
}
}
}
#[derive(Copy, Clone, PartialEq, Debug, Eq)]
pub enum FbxMapping {
ByPolygon,
ByPolygonVertex,
ByVertex,
ByEdge,
AllSame,
}
impl FbxMapping {
pub fn from_string<P: AsRef<str>>(value: P) -> Result<Self, FbxError> {
match value.as_ref() {
"ByPolygon" => Ok(FbxMapping::ByPolygon),
"ByPolygonVertex" => Ok(FbxMapping::ByPolygonVertex),
"ByVertex" | "ByVertice" => Ok(FbxMapping::ByVertex),
"ByEdge" => Ok(FbxMapping::ByEdge),
"AllSame" => Ok(FbxMapping::AllSame),
_ => Err(FbxError::InvalidMapping),
}
}
}
#[derive(Copy, Clone, PartialEq, Debug, Eq)]
pub enum FbxReference {
Direct,
IndexToDirect,
}
impl FbxReference {
pub fn from_string<P: AsRef<str>>(value: P) -> Result<Self, FbxError> {
match value.as_ref() {
"Direct" => Ok(FbxReference::Direct),
"IndexToDirect" => Ok(FbxReference::IndexToDirect),
"Index" => Ok(FbxReference::IndexToDirect),
_ => Err(FbxError::InvalidReference),
}
}
}
pub struct FbxLayerElement<T> {
pub elements: Vec<T>,
pub index: Vec<i32>,
pub mapping: FbxMapping,
pub reference: FbxReference,
}
impl<T> FbxLayerElement<T> {
pub fn new<M, P>(
nodes: &FbxNodeContainer,
container_node: Handle<FbxNode>,
data_name: P,
mapper: M,
) -> Result<Self, FbxError>
where
M: FnOnce(&[FbxAttribute]) -> Result<Vec<T>, FbxError>,
P: AsRef<str>,
{
let map_type_node = nodes.get_by_name(container_node, "MappingInformationType")?;
let mapping = FbxMapping::from_string(map_type_node.get_attrib(0)?.as_string())?;
let ref_type_node = nodes.get_by_name(container_node, "ReferenceInformationType")?;
let mut reference = FbxReference::from_string(ref_type_node.get_attrib(0)?.as_string())?;
let array_node_handle = nodes.find(container_node, data_name.as_ref())?;
let array_node = nodes.get_by_name(array_node_handle, "a")?;
let mut index = Vec::new();
// This check is needed because FBX expects materials to be always IndexToDirect
// See: https://developer.blender.org/D402
if data_name.as_ref() != "Materials" {
if reference == FbxReference::IndexToDirect {
let index_node = nodes.find(
container_node,
format!("{}Index", data_name.as_ref()).as_str(),
)?;
let index_array_node = nodes.get_by_name(index_node, "a")?;
for attribute in index_array_node.attributes() {
let idx = attribute.as_i32()?;
index.push(if idx < 0 { fix_index(idx) as i32 } else { idx });
}
}
} else {
// As said earlier, this is actually direct mapping in case of Materials, so fix this.
// Nice specification, Autodesk, very consistent, good job.
reference = FbxReference::Direct;
}
Ok(Self {
elements: mapper(array_node.attributes())?,
index,
mapping,
reference,
})
}
fn map_index(&self, index: usize) -> Result<usize, FbxError> {
match self.reference {
FbxReference::Direct => Ok(index),
FbxReference::IndexToDirect => {
Ok(*self.index.get(index).ok_or(FbxError::IndexOutOfBounds)? as usize)
}
}
}
/// Returns reference to element at given index. There are two kind of indices:
/// 1) `index` - direct index of element
/// 2) `index_in_polygon` - global index but relative to polygon. Such index is used
/// mostly for normals because any mesh can contain few normals per vertex and such
/// normals are unpacked into plain array. For example we have two polygons,
///
/// A____B_____C
/// | | |
/// | | |
/// |____|____|
/// D E F
///
/// As you can see these polygons share BE edge, vertices B and E can have two normal
/// vectors if these faces are not coplanar. So to handle this case FBX stores unpacked
/// array of normals - in this case 4 normals *per-face* like this (A,B,E,D,B*,C,F,E*).
/// Situation may become worse if we have arbitrary polygon - it must be triangulated:
///
/// A____B_____C
/// |\ |\ |
/// | \ | \ |
/// |___\|___\|
/// D E F
///
/// So here we have AED, ABE, BFE, BCF triangles which has same set of normals as before.
/// To correctly fetch normals from array after triangulation we have to do it like so:
///
/// counter = 0
/// Iterate over each initial *non-triangulated* polygon
/// triangles = triangulate polygon
/// for each triangle in triangles
/// for each index in triangle
/// fetch normal at (counter + index)
/// counter += polygon vertex count
///
/// # Notes
///
/// FBX uses a lot of optimizations to store data as compact as possible, so there are
/// separate arrays of vertex positions and normals instead of array of vertices that has
/// position *and* normal together. This fact introduces a lot of head ache when you need
/// to "inflate" such "packed" data in form that suitable for GPU.
///
/// # Useful links
///
/// Check this article:
/// https://banexdevblog.wordpress.com/2014/06/23/a-quick-tutorial-about-the-fbx-ascii-format/
/// it has some nice pictures which will clarify this mess.
///
pub fn get(&self, index: usize, index_in_polygon: usize) -> Result<&T, FbxError> {
Ok(match self.mapping {
FbxMapping::ByPolygon | FbxMapping::ByVertex | FbxMapping::ByEdge => self
.elements
.get(self.map_index(index)?)
.ok_or(FbxError::IndexOutOfBounds)
.unwrap(),
FbxMapping::ByPolygonVertex => self
.elements
.get(self.map_index(index_in_polygon).unwrap())
.ok_or(FbxError::IndexOutOfBounds)
.unwrap(),
FbxMapping::AllSame => self
.elements
.first()
.ok_or(FbxError::IndexOutOfBounds)
.unwrap(),
})
}
}
pub fn attributes_to_vec3_array(
attributes: &[FbxAttribute],
) -> Result<Vec<Vector3<f32>>, FbxError> {
let mut out_container = Vec::with_capacity(attributes.len() / 3);
for chunk in attributes.chunks_exact(3) {
out_container.push(Vector3::new(
chunk[0].as_f32()?,
chunk[1].as_f32()?,
chunk[2].as_f32()?,
));
}
Ok(out_container)
}
pub fn make_vec3_container<P: AsRef<str>>(
nodes: &FbxNodeContainer,
container_node: Handle<FbxNode>,
data_name: P,
) -> Result<FbxLayerElement<Vector3<f32>>, FbxError> {
FbxLayerElement::new(nodes, container_node, data_name, |attributes| {
attributes_to_vec3_array(attributes)
})
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::core::algebra::{Matrix4, Vector3};
use crate::{
core::pool::Handle,
resource::fbx::{
document::{FbxNode, FbxNodeContainer},
scene::FbxComponent,
},
};
pub struct FbxModel {
pub name: String,
pub pre_rotation: Vector3<f32>,
pub post_rotation: Vector3<f32>,
pub rotation_offset: Vector3<f32>,
pub rotation_pivot: Vector3<f32>,
pub scaling_offset: Vector3<f32>,
pub scaling_pivot: Vector3<f32>,
pub rotation: Vector3<f32>,
pub scale: Vector3<f32>,
pub translation: Vector3<f32>,
pub geometric_translation: Vector3<f32>,
pub geometric_rotation: Vector3<f32>,
pub geometric_scale: Vector3<f32>,
pub inv_bind_transform: Matrix4<f32>,
pub geoms: Vec<Handle<FbxComponent>>,
/// List of handles of materials
pub materials: Vec<Handle<FbxComponent>>,
/// List of handles of animation curve nodes
pub animation_curve_nodes: Vec<Handle<FbxComponent>>,
/// List of handles of children models
pub children: Vec<Handle<FbxComponent>>,
/// Handle to light component
pub light: Handle<FbxComponent>,
}
impl FbxModel {
pub fn read(
model_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<FbxModel, String> {
let mut name = String::from("Unnamed");
let model_node = nodes.get(model_node_handle);
if let Ok(name_attrib) = model_node.get_attrib(1) {
name = name_attrib.as_string();
}
// Remove prefix
if name.starts_with("Model::") {
name = name.chars().skip(7).collect();
}
let mut model = FbxModel {
name,
pre_rotation: Vector3::default(),
post_rotation: Vector3::default(),
rotation_offset: Vector3::default(),
rotation_pivot: Vector3::default(),
scaling_offset: Vector3::default(),
scaling_pivot: Vector3::default(),
rotation: Vector3::default(),
scale: Vector3::new(1.0, 1.0, 1.0),
translation: Vector3::default(),
geometric_translation: Vector3::default(),
geometric_rotation: Vector3::default(),
geometric_scale: Vector3::new(1.0, 1.0, 1.0),
inv_bind_transform: Matrix4::identity(),
geoms: Vec::new(),
materials: Vec::new(),
animation_curve_nodes: Vec::new(),
children: Vec::new(),
light: Handle::NONE,
};
let properties70_node_handle = nodes.find(model_node_handle, "Properties70")?;
let properties70_node = nodes.get(properties70_node_handle);
for property_handle in properties70_node.children() {
let property_node = nodes.get(*property_handle);
let name_attrib = property_node.get_attrib(0)?;
match name_attrib.as_string().as_str() {
"Lcl Translation" => model.translation = property_node.get_vec3_at(4)?,
"Lcl Rotation" => model.rotation = property_node.get_vec3_at(4)?,
"Lcl Scaling" => model.scale = property_node.get_vec3_at(4)?,
"PreRotation" => model.pre_rotation = property_node.get_vec3_at(4)?,
"PostRotation" => model.post_rotation = property_node.get_vec3_at(4)?,
"RotationOffset" => model.rotation_offset = property_node.get_vec3_at(4)?,
"RotationPivot" => model.rotation_pivot = property_node.get_vec3_at(4)?,
"ScalingOffset" => model.scaling_offset = property_node.get_vec3_at(4)?,
"ScalingPivot" => model.scaling_pivot = property_node.get_vec3_at(4)?,
"GeometricTranslation" => {
model.geometric_translation = property_node.get_vec3_at(4)?
}
"GeometricScaling" => model.geometric_scale = property_node.get_vec3_at(4)?,
"GeometricRotation" => model.geometric_rotation = property_node.get_vec3_at(4)?,
_ => (), // Unused properties
}
}
Ok(model)
}
}
@@ -0,0 +1,77 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::pool::{Handle, Pool},
resource::fbx::{
document::{FbxNode, FbxNodeContainer},
scene::FbxComponent,
},
};
use std::path::PathBuf;
#[derive(Debug)]
pub struct FbxTexture {
pub filename: PathBuf,
pub content: Vec<u8>,
pub ancestor: Handle<FbxComponent>,
}
impl FbxTexture {
pub(in crate::resource::fbx) fn read(
texture_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Self, String> {
let mut texture = FbxTexture {
filename: PathBuf::new(),
content: Default::default(),
ancestor: Default::default(),
};
if let Ok(relative_file_name_node) =
nodes.get_by_name(texture_node_handle, "RelativeFilename")
{
// Since most of FBX files were made on Windows in 3ds MAX or Maya, it contains
// paths with double back slashes, we must fix this so this path can be used
// on linux.
if let Ok(attrib) = relative_file_name_node.get_attrib(0) {
let str_path = attrib.as_string().replace('\\', "/");
texture.filename = PathBuf::from(str_path);
}
}
Ok(texture)
}
/// Tries to resolve the entire chain of material nodes and find texture path.
pub(in crate::resource::fbx) fn get_root_file_path(
&self,
components: &Pool<FbxComponent>,
) -> PathBuf {
if self.filename == PathBuf::default() {
components
.try_borrow(self.ancestor)
.ok()
.and_then(|parent| parent.as_texture().ok())
.map(|texture| texture.get_root_file_path(components))
.unwrap_or_default()
} else {
self.filename.clone()
}
}
}
@@ -0,0 +1,54 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
core::pool::Handle,
resource::fbx::document::{attribute::FbxAttribute, FbxNode, FbxNodeContainer},
};
use base64::Engine;
pub struct FbxVideo {
pub content: Vec<u8>,
}
impl FbxVideo {
pub(in crate::resource::fbx) fn read(
video_node_handle: Handle<FbxNode>,
nodes: &FbxNodeContainer,
) -> Result<Self, String> {
if let Ok(content_node) = nodes.get_by_name(video_node_handle, "Content") {
let attrib = content_node.get_attrib(0)?;
let content = match attrib {
FbxAttribute::String(base64) => base64::engine::general_purpose::STANDARD
.decode(base64)
.expect("FBX cannot contain invalid base64-encoded data."),
FbxAttribute::RawData(raw) => raw.clone(),
_ => Default::default(),
};
Ok(Self { content })
} else {
Ok(Self {
content: Default::default(),
})
}
}
}
+689
View File
@@ -0,0 +1,689 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use super::iter::*;
use super::simplify::*;
use crate::core::log::Log;
use crate::core::math::curve::{Curve, CurveKey, CurveKeyKind};
use crate::core::pool::Handle;
use crate::fxhash::FxHashSet;
use crate::generic_animation::container::{TrackDataContainer, TrackValueKind};
use crate::generic_animation::track::Track;
use crate::generic_animation::value::{ValueBinding, ValueType};
use crate::scene::animation::Animation;
use crate::scene::graph::Graph;
use crate::scene::mesh::Mesh;
use crate::scene::node::Node;
use fyrox_animation::track::TrackBinding;
use fyrox_graph::SceneGraph;
use gltf::animation::util::ReadOutputs;
use gltf::animation::Channel;
use gltf::animation::{Interpolation, Property};
use gltf::Buffer;
type Result<T> = std::result::Result<T, ()>;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ImportedBinding {
Position,
Rotation,
Scale,
Weight(usize),
}
impl ImportedBinding {
fn epsilon(&self) -> f32 {
match self {
ImportedBinding::Position => 0.001,
ImportedBinding::Rotation => std::f32::consts::PI / 180.0,
ImportedBinding::Scale => 0.1,
ImportedBinding::Weight(_) => 0.001,
}
}
fn max_step(&self) -> f32 {
match self {
ImportedBinding::Position => f32::INFINITY,
ImportedBinding::Rotation => std::f32::consts::PI / 4.0,
ImportedBinding::Scale => f32::INFINITY,
ImportedBinding::Weight(_) => f32::INFINITY,
}
}
fn morph_index(&self) -> Result<usize> {
match self {
ImportedBinding::Position => Err(()),
ImportedBinding::Rotation => Err(()),
ImportedBinding::Scale => Err(()),
ImportedBinding::Weight(i) => Ok(*i),
}
}
fn kind(&self) -> TrackValueKind {
match self {
ImportedBinding::Position => TrackValueKind::Vector3,
ImportedBinding::Rotation => TrackValueKind::UnitQuaternion,
ImportedBinding::Scale => TrackValueKind::Vector3,
ImportedBinding::Weight(_) => TrackValueKind::Real,
}
}
fn value_binding(&self) -> ValueBinding {
match self {
ImportedBinding::Position => ValueBinding::Position,
ImportedBinding::Rotation => ValueBinding::Rotation,
ImportedBinding::Scale => ValueBinding::Scale,
ImportedBinding::Weight(i) => ValueBinding::Property {
name: format!("blend_shapes[{i}].weight").into(),
value_type: ValueType::F32,
},
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct ImportedTarget {
pub handle: Handle<Node>,
pub binding: ImportedBinding,
}
impl ImportedTarget {
fn morph_index(&self) -> Result<usize> {
self.binding.morph_index()
}
fn kind(&self) -> TrackValueKind {
self.binding.kind()
}
fn value_binding(&self) -> ValueBinding {
self.binding.value_binding()
}
fn value_in_graph(&self, graph: &Graph) -> Option<Box<[f32]>> {
let node: &Node = graph.try_get_node(self.handle).ok()?;
match self.binding {
ImportedBinding::Position => {
Some(node.local_transform().position().data.as_slice().into())
}
ImportedBinding::Rotation => Some(
node.local_transform()
.rotation()
.get_value_ref()
.coords
.data
.as_slice()
.into(),
),
ImportedBinding::Scale => Some(node.local_transform().scale().data.as_slice().into()),
ImportedBinding::Weight(index) => match node.cast::<Mesh>() {
Some(mesh) => mesh.blend_shapes().get(index).map(|s| [s.weight].into()),
None => None,
},
}
}
}
#[derive(Debug, Clone)]
pub struct ImportedTrack {
pub target: ImportedTarget,
pub curves: Box<[Vec<CurveKey>]>,
}
impl ImportedTrack {
fn new(target: ImportedTarget) -> Self {
Self {
target,
curves: match target.binding {
ImportedBinding::Position => <[Vec<CurveKey>; 3]>::default().into(),
ImportedBinding::Rotation => <[Vec<CurveKey>; 4]>::default().into(),
ImportedBinding::Scale => <[Vec<CurveKey>; 3]>::default().into(),
ImportedBinding::Weight(_) => <[Vec<CurveKey>; 1]>::default().into(),
},
}
}
fn simplify_curves(&mut self) {
for curve in self.curves.iter_mut() {
*curve = simplify(
curve.as_slice(),
self.target.binding.epsilon(),
self.target.binding.max_step(),
);
}
}
fn fixed_value(&self) -> Option<Box<[f32]>> {
let mut result: Box<[f32]> = match self.target.binding {
ImportedBinding::Weight(_) => <[f32; 1]>::default().into(),
ImportedBinding::Rotation => <[f32; 4]>::default().into(),
_ => <[f32; 3]>::default().into(),
};
for (i, curve) in self.curves.iter().enumerate() {
if curve.len() != 1 {
return None;
}
result[i] = curve[0].value;
}
Some(result)
}
fn is_fixed_to_graph(&self, graph: &Graph) -> bool {
if let (Some(x), Some(y)) = (self.target.value_in_graph(graph), self.fixed_value()) {
for (x0, y0) in x.iter().zip(y.iter()) {
if f32::abs(x0 - y0) > self.target.binding.epsilon() {
return false;
}
}
true
} else {
false
}
}
fn into_track(self) -> (Handle<Node>, Track) {
let mut data = TrackDataContainer::new(self.target.kind());
for (i, curve) in self.curves.into_vec().into_iter().enumerate() {
data.curves_mut()[i] = Curve::from(curve);
}
(
self.target.handle,
Track::new(data, self.target.value_binding()),
)
}
}
struct ImportedAnimation {
pub name: Box<str>,
pub start: f32,
pub end: f32,
pub tracks: Vec<ImportedTrack>,
}
impl ImportedAnimation {
fn targets(&self) -> impl Iterator<Item = ImportedTarget> + '_ {
self.tracks.iter().map(|t| t.target)
}
fn get(&self, target: ImportedTarget) -> Option<&ImportedTrack> {
self.tracks.iter().find(|t| t.target == target)
}
fn remove_target(&mut self, target: ImportedTarget) {
self.tracks.retain(|t| t.target != target);
}
fn simplify_curves(&mut self) {
for t in self.tracks.iter_mut() {
t.simplify_curves();
}
}
fn into_animation(self) -> Animation {
let mut result = Animation::default();
result.set_name(self.name);
result.set_time_slice(self.start..self.end);
for t in self.tracks {
let (node, track) = t.into_track();
result.add_track_with_binding(TrackBinding::new(node), track);
}
result
}
}
fn all_targets(source: &[ImportedAnimation]) -> impl Iterator<Item = ImportedTarget> {
let mut set: FxHashSet<ImportedTarget> = FxHashSet::default();
for anim in source {
set.extend(anim.targets());
}
set.into_iter()
}
fn target_is_fixed_in_all(
target: ImportedTarget,
anims: &[ImportedAnimation],
graph: &Graph,
) -> bool {
for anim in anims {
if let Some(track) = anim.get(target) {
if !track.is_fixed_to_graph(graph) {
return false;
}
}
}
true
}
fn remove_fixed_targets(anims: &mut [ImportedAnimation], graph: &Graph) {
for target in all_targets(anims) {
if target_is_fixed_in_all(target, anims, graph) {
for anim in anims.iter_mut() {
anim.remove_target(target);
}
}
}
}
/// Extract a list of Animations from the give glTF document, if that document contains any.
/// The resulting list of animations is not guaranteed to be the same length as the list of animations
/// in the document. If any error is encountered in translating an animation from glTF to Fyrox,
/// that animation will be excluded from the resulting list and an error message will be logged.
///
/// * `doc`: The document in which to find the animations.
///
/// * `node_handles`: A slice containing a [Handle] for every node defined in the document, in that order, so that
/// a handle can be looked up using the index of a node within the document. Animations in glTF specify their target
/// nodes by their index within the node list of the document, and these indices need to be translated into handles.
///
/// * `buffers`: A slice containing a list of byte-vectors, one for each buffer in the glTF document.
/// Animations in glTF make reference to data stored in the document's list of buffers by index.
/// This slcie allows an index into the document's list of buffers to be translated into actual bytes of data.
pub fn import_animations(
doc: &gltf::Document,
node_handles: &[Handle<Node>],
graph: &Graph,
buffers: &[Vec<u8>],
) -> Vec<Animation> {
let mut imports: Vec<ImportedAnimation> = Vec::with_capacity(doc.animations().len());
for animation in doc.animations() {
if let Ok(mut import) = import_animation(&animation, node_handles, buffers) {
import.simplify_curves();
imports.push(import);
} else {
Log::err(format!(
"Failed to import animation: {}",
animation.name().unwrap_or("[Unnamed]")
));
}
}
remove_fixed_targets(imports.as_mut_slice(), graph);
let mut result: Vec<Animation> = Vec::with_capacity(imports.len());
for import in imports {
result.push(import.into_animation());
}
result
}
fn import_animation(
animation: &gltf::Animation,
node_handles: &[Handle<Node>],
buffers: &[Vec<u8>],
) -> Result<ImportedAnimation> {
let name = animation
.name()
.map(Box::<str>::from)
.unwrap_or(default_animation_name(animation));
let end = animation
.channels()
.map(|c| get_channel_end(&c))
.max_by(f32::total_cmp)
.unwrap_or(0.0);
Ok(ImportedAnimation {
name,
start: 0.0,
end,
tracks: import_channels(animation, node_handles, buffers)?,
})
}
fn get_channel_end(channel: &Channel) -> f32 {
get_accessor_max(channel.sampler().input()).unwrap_or(0.0)
}
fn get_accessor_max(accessor: gltf::Accessor) -> Option<f32> {
let max = accessor.max()?;
let vec: &Vec<gltf::json::Value> = max.as_array()?;
Some(vec.first()?.as_f64()? as f32)
}
fn default_animation_name(animation: &gltf::Animation) -> Box<str> {
format!("Animation {}", animation.index()).into()
}
/// Build the given `build_animation` based on the given `source_animation`.
fn import_channels(
source_animation: &gltf::Animation,
node_handles: &[Handle<Node>],
buffers: &[Vec<u8>],
) -> Result<Vec<ImportedTrack>> {
let mut result: Vec<ImportedTrack> = Vec::with_capacity(source_animation.channels().count());
for channel in source_animation.channels() {
let target_index = channel.target().node().index();
let handle = *node_handles.get(target_index).ok_or(())?;
if let Property::MorphTargetWeights = channel.target().property() {
import_weight_channel(&channel, handle, &mut result, |buf: Buffer| {
buffers.get(buf.index()).map(Vec::as_slice)
})?
} else {
import_transform_channel(&channel, handle, &mut result, |buf: Buffer| {
buffers.get(buf.index()).map(Vec::as_slice)
})?
};
}
Ok(result)
}
fn import_transform_channel<'a, 's, F>(
channel: &'a Channel,
handle: Handle<Node>,
result: &mut Vec<ImportedTrack>,
get_buffer_data: F,
) -> Result<()>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let track: ImportedTrack = import_sampler(channel, handle, get_buffer_data)?;
result.push(track);
Ok(())
}
fn import_weight_channel<'a, 's, F>(
channel: &'a Channel,
target: Handle<Node>,
result: &mut Vec<ImportedTrack>,
get_buffer_data: F,
) -> Result<()>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let target_count = count_morph_targets(&channel.target().node());
for i in 0..target_count {
let target = ImportedTarget {
handle: target,
binding: ImportedBinding::Weight(i),
};
let track = match channel.sampler().interpolation() {
Interpolation::Linear => import_simple_morph_sampler(
channel,
target,
target_count,
CurveKeyKind::Linear,
get_buffer_data.clone(),
)?,
Interpolation::Step => import_simple_morph_sampler(
channel,
target,
target_count,
CurveKeyKind::Constant,
get_buffer_data.clone(),
)?,
Interpolation::CubicSpline => {
import_cubic_morph_sampler(channel, target, target_count, get_buffer_data.clone())?
}
};
result.push(track);
}
Ok(())
}
fn count_morph_targets(target: &gltf::Node) -> usize {
if let Some(mesh) = target.mesh() {
if let Some(prim) = mesh.primitives().next() {
prim.morph_targets().count()
} else {
0
}
} else {
0
}
}
fn import_sampler<'a, 's, F>(
channel: &'a Channel,
target_handle: Handle<Node>,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
if let Property::Rotation = channel.target().property() {
let target = ImportedTarget {
handle: target_handle,
binding: ImportedBinding::Rotation,
};
match channel.sampler().interpolation() {
Interpolation::Linear => {
import_simple_rotation(channel, target, CurveKeyKind::Linear, get_buffer_data)
}
Interpolation::Step => {
import_simple_rotation(channel, target, CurveKeyKind::Constant, get_buffer_data)
}
Interpolation::CubicSpline => import_cubic_rotation(channel, target, get_buffer_data),
}
} else {
let target = ImportedTarget {
handle: target_handle,
binding: match channel.target().property() {
Property::Translation => ImportedBinding::Position,
Property::Scale => ImportedBinding::Scale,
_ => return Err(()),
},
};
match channel.sampler().interpolation() {
Interpolation::Linear => {
import_simple_sampler(channel, target, CurveKeyKind::Linear, get_buffer_data)
}
Interpolation::Step => {
import_simple_sampler(channel, target, CurveKeyKind::Constant, get_buffer_data)
}
Interpolation::CubicSpline => import_cubic_sampler(channel, target, get_buffer_data),
}
}
}
fn import_simple_sampler<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
kind: CurveKeyKind,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::Translations(iter) => iter,
ReadOutputs::Scales(iter) => iter,
ReadOutputs::Rotations(_) => return Err(()),
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track = ImportedTrack::new(target);
for i in 0..3 {
let curve_keys: Vec<CurveKey> = inputs
.clone()
.zip(out_iter.clone())
.map(|(time, o)| CurveKey::new(time, o[i], kind.clone()))
.collect::<Vec<_>>();
track.curves[i] = curve_keys;
}
Ok(track)
}
fn import_simple_morph_sampler<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
morph_count: usize,
kind: CurveKeyKind,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::MorphTargetWeights(weights) => weights.into_f32(),
_ => return Err(()),
};
let out_iter = select_iterator(out_iter, target.morph_index()?, morph_count);
let mut track = ImportedTrack::new(target);
track.curves[0] = inputs
.zip(out_iter)
.map(|(time, o)| CurveKey::new(time, o * 100.0, kind.clone()))
.collect();
Ok(track)
}
fn import_cubic_sampler<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::Translations(iter) => iter,
ReadOutputs::Scales(iter) => iter,
ReadOutputs::Rotations(_) => return Err(()),
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track: ImportedTrack = ImportedTrack::new(target);
for i in 0..3 {
let curve_keys: Vec<CurveKey> = iter_cubic_data(inputs.clone(), out_iter.clone())
.map(|(time, o)| {
CurveKey::new(
time,
o[1][i],
CurveKeyKind::Cubic {
left_tangent: o[0][i],
right_tangent: o[2][i],
},
)
})
.collect::<Vec<_>>();
track.curves[i] = curve_keys;
}
Ok(track)
}
fn import_cubic_morph_sampler<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
morph_count: usize,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::MorphTargetWeights(weights) => weights.into_f32(),
_ => return Err(()),
};
let out_iter = select_iterator(out_iter, target.morph_index()?, morph_count);
let iter = iter_cubic_data(inputs, out_iter);
let mut track = ImportedTrack::new(target);
let curve_keys: Vec<CurveKey> = iter
.map(|(time, o)| {
CurveKey::new(
time,
o[1] * 100.0,
CurveKeyKind::Cubic {
left_tangent: o[0] * 100.0,
right_tangent: o[2] * 100.0,
},
)
})
.collect();
track.curves[0] = curve_keys;
Ok(track)
}
fn import_simple_rotation<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
kind: CurveKeyKind,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::Translations(_) => return Err(()),
ReadOutputs::Scales(_) => return Err(()),
ReadOutputs::Rotations(iter) => iter.into_f32(),
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track = ImportedTrack::new(target);
for i in 0..4 {
let curve_keys: Vec<CurveKey> = inputs
.clone()
.zip(out_iter.clone())
.map(|(time, o)| CurveKey::new(time, o[i], kind.clone()))
.collect::<Vec<_>>();
track.curves[i] = curve_keys;
}
Ok(track)
}
fn import_cubic_rotation<'a, 's, F>(
channel: &'a gltf::animation::Channel,
target: ImportedTarget,
get_buffer_data: F,
) -> Result<ImportedTrack>
where
F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>,
{
let inputs = channel
.reader(get_buffer_data.clone())
.read_inputs()
.ok_or(())?;
let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?;
let out_iter = match outputs {
ReadOutputs::Translations(_) => return Err(()),
ReadOutputs::Scales(_) => return Err(()),
ReadOutputs::Rotations(iter) => iter.into_f32(),
ReadOutputs::MorphTargetWeights(_) => return Err(()),
};
let mut track = ImportedTrack::new(target);
for i in 0..4 {
let curve_keys: Vec<CurveKey> = iter_cubic_data(inputs.clone(), out_iter.clone())
.map(|(time, o)| {
let (in_tang, value, out_tang) = (o[0], o[1], o[2]);
CurveKey::new(
time,
value[i],
CurveKeyKind::Cubic {
left_tangent: in_tang[i],
right_tangent: out_tang[i],
},
)
})
.collect::<Vec<_>>();
track.curves[i] = curve_keys;
}
Ok(track)
}
impl CurvePoint for CurveKey {
fn x(&self) -> f32 {
self.location
}
fn y(&self) -> f32 {
self.value
}
}
@@ -0,0 +1,617 @@
(
name: "GLTFShader",
resources: [
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: Normal),
binding: 1
),
(
name: "metallicRoughnessTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 2
),
(
name: "heightTexture",
kind: Texture(kind: Sampler2D, fallback: Black),
binding: 3
),
(
name: "emissionTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 4
),
(
name: "lightmapTexture",
kind: Texture(kind: Sampler2D, fallback: Black),
binding: 5
),
(
name: "aoTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 6
),
(
name: "blendShapesStorage",
kind: Texture(kind: Sampler3D, fallback: Volume),
binding: 7
),
(
name: "properties",
kind: PropertyGroup([
(
name: "texCoordScale",
kind: Vector2(value: (1.0, 1.0)),
),
(
name: "layerIndex",
kind: UInt(value: 0),
),
(
name: "emissionStrength",
kind: Vector3(value: (2.0, 2.0, 2.0)),
),
(
name: "diffuseColor",
kind: Color(r: 255, g: 255, b: 255, a: 255),
),
(
name: "parallaxCenter",
kind: Float(value: 0.0),
),
(
name: "parallaxScale",
kind: Float(value: 0.08),
),
(
name: "metallicFactor",
kind: Float(value: 1.0)
),
(
name: "roughnessFactor",
kind: Float(value: 1.0)
)
]),
binding: 0
),
(
name: "fyrox_instanceData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 1
),
(
name: "fyrox_boneMatrices",
kind: PropertyGroup([
// Autogenerated
]),
binding: 2
),
(
name: "fyrox_graphicsSettings",
kind: PropertyGroup([
// Autogenerated
]),
binding: 3
),
(
name: "fyrox_cameraData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 4
),
(
name: "fyrox_lightData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 5
),
],
passes: [
(
name: "GBuffer",
draw_parameters: DrawParameters(
cull_face: Some(Back),
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
layout(location = 2) in vec3 vertexNormal;
layout(location = 3) in vec4 vertexTangent;
layout(location = 4) in vec4 boneWeights;
layout(location = 5) in vec4 boneIndices;
layout(location = 6) in vec2 vertexSecondTexCoord;
out vec3 position;
out vec3 normal;
out vec2 texCoord;
out vec3 tangent;
out vec3 binormal;
out vec2 secondTexCoord;
void main()
{
vec4 localPosition = vec4(0);
vec3 localNormal = vec3(0);
vec3 localTangent = vec3(0);
vec4 inputPosition = vec4(vertexPosition, 1.0);
vec3 inputNormal = vertexNormal;
vec3 inputTangent = vertexTangent.xyz;
for (int i = 0; i < fyrox_instanceData.blendShapesCount; ++i) {
TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(blendShapesStorage, gl_VertexID, i);
float weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4];
inputPosition.xyz += offsets.position * weight;
inputNormal += offsets.normal * weight;
inputTangent += offsets.tangent * weight;
}
if (fyrox_instanceData.useSkeletalAnimation)
{
int i0 = int(boneIndices.x);
int i1 = int(boneIndices.y);
int i2 = int(boneIndices.z);
int i3 = int(boneIndices.w);
mat4 m0 = fyrox_boneMatrices.matrices[i0];
mat4 m1 = fyrox_boneMatrices.matrices[i1];
mat4 m2 = fyrox_boneMatrices.matrices[i2];
mat4 m3 = fyrox_boneMatrices.matrices[i3];
localPosition += m0 * inputPosition * boneWeights.x;
localPosition += m1 * inputPosition * boneWeights.y;
localPosition += m2 * inputPosition * boneWeights.z;
localPosition += m3 * inputPosition * boneWeights.w;
localNormal += mat3(m0) * inputNormal * boneWeights.x;
localNormal += mat3(m1) * inputNormal * boneWeights.y;
localNormal += mat3(m2) * inputNormal * boneWeights.z;
localNormal += mat3(m3) * inputNormal * boneWeights.w;
localTangent += mat3(m0) * inputTangent * boneWeights.x;
localTangent += mat3(m1) * inputTangent * boneWeights.y;
localTangent += mat3(m2) * inputTangent * boneWeights.z;
localTangent += mat3(m3) * inputTangent * boneWeights.w;
}
else
{
localPosition = inputPosition;
localNormal = inputNormal;
localTangent = inputTangent;
}
mat3 nm = mat3(fyrox_instanceData.worldMatrix);
normal = normalize(nm * localNormal);
tangent = normalize(nm * localTangent);
binormal = normalize(vertexTangent.w * cross(normal, tangent));
texCoord = vertexTexCoord;
position = vec3(fyrox_instanceData.worldMatrix * localPosition);
secondTexCoord = vertexSecondTexCoord;
gl_Position = fyrox_instanceData.worldViewProjection * localPosition;
}
"#,
fragment_shader:
r#"
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outAmbient;
layout(location = 3) out vec4 outMaterial;
layout(location = 4) out uint outDecalMask;
in vec3 position;
in vec3 normal;
in vec2 texCoord;
in vec3 tangent;
in vec3 binormal;
in vec2 secondTexCoord;
void main()
{
mat3 tangentSpace = mat3(tangent, binormal, normal);
vec3 toFragment = normalize(position - fyrox_cameraData.position);
vec2 tc;
if (fyrox_graphicsSettings.usePOM) {
vec3 toFragmentTangentSpace = normalize(transpose(tangentSpace) * toFragment);
tc = S_ComputeParallaxTextureCoordinates(
heightTexture,
toFragmentTangentSpace,
texCoord * properties.texCoordScale,
properties.parallaxCenter,
properties.parallaxScale
);
} else {
tc = texCoord * properties.texCoordScale;
}
outColor = properties.diffuseColor * texture(diffuseTexture, tc);
// Alpha test.
if (outColor.a < 0.5) {
discard;
}
outColor.a = 1.0;
vec4 n = normalize(texture(normalTexture, tc) * 2.0 - 1.0);
outNormal = vec4(normalize(tangentSpace * n.xyz) * 0.5 + 0.5, 1.0);
outMaterial.x = properties.metallicFactor * texture(metallicRoughnessTexture, tc).b; // Metallic
outMaterial.y = properties.roughnessFactor * texture(metallicRoughnessTexture, tc).g; // Roughness
outMaterial.z = texture(aoTexture, tc).r;
outMaterial.a = 1.0;
outAmbient.xyz = properties.emissionStrength * texture(emissionTexture, tc).rgb + texture(lightmapTexture, secondTexCoord).rgb;
outAmbient.a = 1.0;
outDecalMask = properties.layerIndex;
}
"#,
),
(
name: "Forward",
draw_parameters: DrawParameters(
cull_face: Some(Back),
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: SrcAlpha,
dfactor: OneMinusSrcAlpha,
alpha_sfactor: SrcAlpha,
alpha_dfactor: OneMinusSrcAlpha,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
layout(location = 4) in vec4 boneWeights;
layout(location = 5) in vec4 boneIndices;
out vec3 position;
out vec2 texCoord;
void main()
{
vec4 localPosition = vec4(0);
vec4 inputPosition = vec4(vertexPosition, 1.0);
for (int i = 0; i < fyrox_instanceData.blendShapesCount; ++i) {
TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(blendShapesStorage, gl_VertexID, i);
float weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4];
inputPosition.xyz += offsets.position * weight;
}
if (fyrox_instanceData.useSkeletalAnimation)
{
int i0 = int(boneIndices.x);
int i1 = int(boneIndices.y);
int i2 = int(boneIndices.z);
int i3 = int(boneIndices.w);
mat4 m0 = fyrox_boneMatrices.matrices[i0];
mat4 m1 = fyrox_boneMatrices.matrices[i1];
mat4 m2 = fyrox_boneMatrices.matrices[i2];
mat4 m3 = fyrox_boneMatrices.matrices[i3];
localPosition += m0 * inputPosition * boneWeights.x;
localPosition += m1 * inputPosition * boneWeights.y;
localPosition += m2 * inputPosition * boneWeights.z;
localPosition += m3 * inputPosition * boneWeights.w;
}
else
{
localPosition = inputPosition;
}
gl_Position = fyrox_instanceData.worldViewProjection * localPosition;
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
FragColor = properties.diffuseColor * texture(diffuseTexture, texCoord);
}
"#,
),
(
name: "DirectionalShadow",
draw_parameters: DrawParameters (
cull_face: Some(Back),
color_write: ColorMask(
red: false,
green: false,
blue: false,
alpha: false,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
layout(location = 4) in vec4 boneWeights;
layout(location = 5) in vec4 boneIndices;
out vec2 texCoord;
void main()
{
vec4 localPosition = vec4(0);
vec4 inputPosition = vec4(vertexPosition, 1.0);
for (int i = 0; i < fyrox_instanceData.blendShapesCount; ++i) {
TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(blendShapesStorage, gl_VertexID, i);
float weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4];
inputPosition.xyz += offsets.position * weight;
}
if (fyrox_instanceData.useSkeletalAnimation)
{
vec4 vertex = vec4(vertexPosition, 1.0);
mat4 m0 = fyrox_boneMatrices.matrices[int(boneIndices.x)];
mat4 m1 = fyrox_boneMatrices.matrices[int(boneIndices.y)];
mat4 m2 = fyrox_boneMatrices.matrices[int(boneIndices.z)];
mat4 m3 = fyrox_boneMatrices.matrices[int(boneIndices.w)];
localPosition += m0 * inputPosition * boneWeights.x;
localPosition += m1 * inputPosition * boneWeights.y;
localPosition += m2 * inputPosition * boneWeights.z;
localPosition += m3 * inputPosition * boneWeights.w;
}
else
{
localPosition = inputPosition;
}
gl_Position = fyrox_instanceData.worldViewProjection * localPosition;
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
void main()
{
if (texture(diffuseTexture, texCoord).a < 0.2) discard;
}
"#,
),
(
name: "SpotShadow",
draw_parameters: DrawParameters (
cull_face: Some(Back),
color_write: ColorMask(
red: false,
green: false,
blue: false,
alpha: false,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
layout(location = 4) in vec4 boneWeights;
layout(location = 5) in vec4 boneIndices;
out vec2 texCoord;
void main()
{
vec4 localPosition = vec4(0);
vec4 inputPosition = vec4(vertexPosition, 1.0);
for (int i = 0; i < fyrox_instanceData.blendShapesCount; ++i) {
TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(blendShapesStorage, gl_VertexID, i);
float weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4];
inputPosition.xyz += offsets.position * weight;
}
if (fyrox_instanceData.useSkeletalAnimation)
{
vec4 vertex = vec4(vertexPosition, 1.0);
mat4 m0 = fyrox_boneMatrices.matrices[int(boneIndices.x)];
mat4 m1 = fyrox_boneMatrices.matrices[int(boneIndices.y)];
mat4 m2 = fyrox_boneMatrices.matrices[int(boneIndices.z)];
mat4 m3 = fyrox_boneMatrices.matrices[int(boneIndices.w)];
localPosition += m0 * inputPosition * boneWeights.x;
localPosition += m1 * inputPosition * boneWeights.y;
localPosition += m2 * inputPosition * boneWeights.z;
localPosition += m3 * inputPosition * boneWeights.w;
}
else
{
localPosition = inputPosition;
}
gl_Position = fyrox_instanceData.worldViewProjection * localPosition;
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
void main()
{
if (texture(diffuseTexture, texCoord).a < 0.2) discard;
}
"#,
),
(
name: "PointShadow",
draw_parameters: DrawParameters (
cull_face: Some(Back),
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
layout(location = 4) in vec4 boneWeights;
layout(location = 5) in vec4 boneIndices;
out vec2 texCoord;
out vec3 worldPosition;
void main()
{
vec4 localPosition = vec4(0);
vec4 inputPosition = vec4(vertexPosition, 1.0);
for (int i = 0; i < fyrox_instanceData.blendShapesCount; ++i) {
TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(blendShapesStorage, gl_VertexID, i);
float weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4];
inputPosition.xyz += offsets.position * weight;
}
if (fyrox_instanceData.useSkeletalAnimation)
{
vec4 vertex = vec4(vertexPosition, 1.0);
mat4 m0 = fyrox_boneMatrices.matrices[int(boneIndices.x)];
mat4 m1 = fyrox_boneMatrices.matrices[int(boneIndices.y)];
mat4 m2 = fyrox_boneMatrices.matrices[int(boneIndices.z)];
mat4 m3 = fyrox_boneMatrices.matrices[int(boneIndices.w)];
localPosition += m0 * inputPosition * boneWeights.x;
localPosition += m1 * inputPosition * boneWeights.y;
localPosition += m2 * inputPosition * boneWeights.z;
localPosition += m3 * inputPosition * boneWeights.w;
}
else
{
localPosition = inputPosition;
}
gl_Position = fyrox_instanceData.worldViewProjection * localPosition;
worldPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz;
texCoord = vertexTexCoord;
}
"#,
fragment_shader:
r#"
in vec2 texCoord;
in vec3 worldPosition;
layout(location = 0) out float depth;
void main()
{
if (texture(diffuseTexture, texCoord).a < 0.2) discard;
depth = length(fyrox_lightData.lightPosition - worldPosition);
}
"#,
)
],
)
+137
View File
@@ -0,0 +1,137 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/// Iterator that skips elements of its inner iterator so that it
/// returns every nth element, starting from a given offset from the
/// start of the inner iterator.
///
/// For example: [0, 1, 2, 3, 4, 5, 6, 7, 8]
/// Becomes: [1, 4, 7]
///
/// In this case, it is returning the second element within each three elements.
#[derive(Debug, Clone)]
pub struct SelectIterator<I> {
source: I,
offset: usize,
window_size: usize,
starting: bool,
}
/// Create an iterator that skips elements of the given iterator to produce a single element
/// within each group of window_size elements. The element returned is `offset` elements from the
/// start of each window.
///
/// For example: select_iterator(iter, 3, 5)
/// If iter produces: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
/// select_iterartor(iter, 3, 5]) procues: [3, 8]
pub fn select_iterator<I>(source: I, offset: usize, window_size: usize) -> SelectIterator<I> {
SelectIterator {
source,
offset,
window_size,
starting: true,
}
}
impl<I> Iterator for SelectIterator<I>
where
I: Iterator,
{
type Item = <I as Iterator>::Item;
fn next(&mut self) -> Option<Self::Item> {
if self.starting {
self.starting = false;
for _ in 0..self.offset {
self.source.next()?;
}
self.source.next()
} else {
for _ in 0..self.window_size - 1 {
self.source.next()?;
}
self.source.next()
}
}
}
pub fn iter_cubic_data<I, O>(
inputs: I,
outputs: O,
) -> impl Iterator<Item = (<I as Iterator>::Item, [<O as Iterator>::Item; 3])>
where
I: Iterator,
O: Iterator,
{
CubicIter { inputs, outputs }
}
#[derive(Debug, Clone)]
pub struct CubicIter<I, O> {
inputs: I,
outputs: O,
}
impl<I, O> Iterator for CubicIter<I, O>
where
I: Iterator,
O: Iterator,
{
type Item = (<I as Iterator>::Item, [<O as Iterator>::Item; 3]);
fn next(&mut self) -> Option<Self::Item> {
Some((
self.inputs.next()?,
[
self.outputs.next()?,
self.outputs.next()?,
self.outputs.next()?,
],
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn select_example1() {
let iter = [0, 1, 2, 3, 4, 5, 6, 7, 8].into_iter();
let result: Vec<_> = select_iterator(iter, 1, 3).collect();
assert!(result.clone().into_iter().eq([1, 4, 7]), "{result:?}");
}
#[test]
fn select_example2() {
let iter = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_iter();
let result: Vec<_> = select_iterator(iter, 3, 5).collect();
assert!(result.clone().into_iter().eq([3, 8]), "{result:?}");
}
#[test]
fn cubic1() {
let input = [0, 1, 2, 3].into_iter();
let output = [-1, 0, 1, 2, 3, 4, 5, 6].into_iter();
let result: Vec<_> = iter_cubic_data(input, output).collect();
assert!(
result
.clone()
.into_iter()
.eq([(0, [-1, 0, 1]), (1, [2, 3, 4])]),
"{result:?}"
);
}
}
+433
View File
@@ -0,0 +1,433 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(missing_docs)]
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::LazyLock,
};
use crate::{
asset::{manager::ResourceManager, state::LoadError, untyped::ResourceKind, Resource},
core::{algebra::Vector4, color::Color, log::Log},
material::{
shader::{Shader, ShaderResource},
Material, MaterialProperty, MaterialResource,
},
resource::{
model::MaterialSearchOptions,
texture::{Texture, TextureError, TextureImportOptions, TextureResource},
},
};
use gltf::{buffer::View, image, Document};
use super::uri;
type Result<T> = std::result::Result<T, GltfMaterialError>;
use crate::resource::texture::TextureMagnificationFilter as FyroxMagFilter;
use crate::resource::texture::TextureMinificationFilter as FyroxMinFilter;
use gltf::texture::MagFilter as GltfMagFilter;
use gltf::texture::MinFilter as GltfMinFilter;
pub static GLTF_SHADER: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
"GltfShader",
embedded_data_source!("gltf_standard.shader"),
|data| {
ShaderResource::new_ok(
uuid!("33ee0142-f345-4c0a-9aca-d1f684a3485b"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
fn convert_mini(filter: GltfMinFilter) -> FyroxMinFilter {
match filter {
GltfMinFilter::Linear => FyroxMinFilter::Linear,
GltfMinFilter::Nearest => FyroxMinFilter::Nearest,
GltfMinFilter::LinearMipmapLinear => FyroxMinFilter::LinearMipMapLinear,
GltfMinFilter::NearestMipmapLinear => FyroxMinFilter::NearestMipMapLinear,
GltfMinFilter::LinearMipmapNearest => FyroxMinFilter::LinearMipMapNearest,
GltfMinFilter::NearestMipmapNearest => FyroxMinFilter::NearestMipMapNearest,
}
}
fn convert_mag(filter: GltfMagFilter) -> FyroxMagFilter {
match filter {
GltfMagFilter::Linear => FyroxMagFilter::Linear,
GltfMagFilter::Nearest => FyroxMagFilter::Nearest,
}
}
use crate::material::{MaterialResourceBinding, MaterialTextureBinding};
use crate::resource::texture::TextureWrapMode as FyroxWrapMode;
use fyrox_resource::builtin::BuiltInResource;
use fyrox_resource::embedded_data_source;
use gltf::texture::WrappingMode as GltfWrapMode;
use uuid::{uuid, Uuid};
fn convert_wrap(mode: GltfWrapMode) -> FyroxWrapMode {
match mode {
GltfWrapMode::Repeat => FyroxWrapMode::Repeat,
GltfWrapMode::ClampToEdge => FyroxWrapMode::ClampToEdge,
GltfWrapMode::MirroredRepeat => FyroxWrapMode::MirroredRepeat,
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum GltfMaterialError {
ShaderLoadFailed,
InvalidIndex,
UnsupportedURI(Box<str>),
TextureNotFound(Box<str>),
Load(LoadError),
Base64(base64::DecodeError),
Texture(TextureError),
}
impl std::error::Error for GltfMaterialError {}
impl Display for GltfMaterialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GltfMaterialError::ShaderLoadFailed => f.write_str("Shader load failed"),
GltfMaterialError::InvalidIndex => f.write_str("Invalid material index"),
GltfMaterialError::UnsupportedURI(uri) => {
write!(f, "Unsupported material URI {uri:?}")
}
GltfMaterialError::TextureNotFound(uri) => write!(f, "Texture not found: {uri:?}"),
GltfMaterialError::Load(error) => Display::fmt(error, f),
GltfMaterialError::Base64(error) => Display::fmt(error, f),
GltfMaterialError::Texture(error) => Display::fmt(error, f),
}
}
}
impl From<LoadError> for GltfMaterialError {
fn from(error: LoadError) -> Self {
GltfMaterialError::Load(error)
}
}
impl From<base64::DecodeError> for GltfMaterialError {
fn from(error: base64::DecodeError) -> Self {
GltfMaterialError::Base64(error)
}
}
impl From<TextureError> for GltfMaterialError {
fn from(error: TextureError) -> Self {
GltfMaterialError::Texture(error)
}
}
pub enum SourceImage<'a> {
External(&'a str),
View(&'a [u8]),
Embedded(Vec<u8>),
}
pub fn decode_base64(source: &str) -> Result<Vec<u8>> {
Ok(uri::decode_base64(source)?)
}
/// Extract a list of [MaterialResource] from the give glTF document, if that document contains any.
/// The resulting list of materials is guaranteed to be the same length as the list of materials
/// in the document so that an index into the document's list of materials will be the same as the index
/// of the matching MaterialResource in the returned list. This is important since the glTF document
/// refers to materials by index.
///
/// * `doc`: The document in which to find the materials.
///
/// * `textures`: A slice containing a [TextureResource] for every texture defined in the document, in that order, so that
/// a texture can be looked up using the index of a texture within the document. Materials in glTF specify their target
/// textures by their index within the node list of the document, and these indices need to be translated into handles.
///
/// * `buffers`: A slice containing a list of byte-vectors, one for each buffer in the glTF document.
/// Animations in glTF make reference to data stored in the document's list of buffers by index.
/// This slcie allows an index into the document's list of buffers to be translated into actual bytes of data.
///
/// * `resource_manager`: A [ResourceManager] makes it possible to access shaders and create materials.
pub async fn import_materials(
gltf: &Document,
textures: &[TextureResource],
) -> Result<Vec<MaterialResource>> {
let mut result: Vec<MaterialResource> = Vec::with_capacity(gltf.materials().len());
for mat in gltf.materials() {
match import_material(mat, textures).await {
Ok(res) => result.push(res),
Err(err) => {
Log::err(format!("glTF material failed to import. Reason: {err:?}"));
result.push(MaterialResource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
Material::default(),
));
}
}
}
Ok(result)
}
async fn import_material(
mat: gltf::Material<'_>,
textures: &[TextureResource],
) -> Result<MaterialResource> {
let shader: ShaderResource = GLTF_SHADER.resource.clone();
if !shader.is_ok() {
return Err(GltfMaterialError::ShaderLoadFailed);
}
let mut result: Material = Material::from_shader(shader);
let pbr = mat.pbr_metallic_roughness();
if let Some(tex) = pbr.base_color_texture() {
set_texture(
&mut result,
"diffuseTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.normal_texture() {
set_texture(
&mut result,
"normalTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = pbr.metallic_roughness_texture() {
set_texture(
&mut result,
"metallicRoughnessTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.emissive_texture() {
set_texture(
&mut result,
"emissionTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.occlusion_texture() {
set_texture(&mut result, "aoTexture", textures, tex.texture().index())?;
}
set_material_color(
&mut result,
"diffuseColor",
Vector4::<f32>::from(pbr.base_color_factor()).into(),
);
let mut emission_strength = mat.emissive_factor();
let emission_factor = mat.emissive_strength().unwrap_or(1.0);
for c in emission_strength.iter_mut() {
*c *= emission_factor;
}
set_material_vector3(&mut result, "emissionStrength", emission_strength);
set_material_scalar(&mut result, "metallicFactor", pbr.metallic_factor());
set_material_scalar(&mut result, "roughnessFactor", pbr.roughness_factor());
Ok(Resource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
result,
))
}
fn set_material_scalar(material: &mut Material, name: &'static str, value: f32) {
let value: MaterialProperty = MaterialProperty::Float(value);
material.set_property(name, value);
}
fn set_material_color(material: &mut Material, name: &'static str, color: Color) {
let value: MaterialProperty = MaterialProperty::Color(color);
material.set_property(name, value);
}
fn set_material_vector3(material: &mut Material, name: &'static str, vector: [f32; 3]) {
let value: MaterialProperty = MaterialProperty::Vector3(vector.into());
material.set_property(name, value);
}
#[allow(dead_code)]
fn set_material_vector4(material: &mut Material, name: &'static str, vector: [f32; 4]) {
let value: MaterialProperty = MaterialProperty::Vector4(vector.into());
material.set_property(name, value);
}
fn set_texture(
material: &mut Material,
name: &'static str,
textures: &[TextureResource],
index: usize,
) -> Result<()> {
let tex: TextureResource = textures
.get(index)
.ok_or(GltfMaterialError::InvalidIndex)?
.clone();
material.bind(
name,
MaterialResourceBinding::Texture(MaterialTextureBinding { value: Some(tex) }),
);
Ok(())
}
pub fn import_images<'a, 'b>(
gltf: &'a Document,
buffers: &'b [Vec<u8>],
) -> Result<Vec<SourceImage<'b>>>
where
'a: 'b,
{
let mut result: Vec<SourceImage> = Vec::new();
for image in gltf.images() {
match image.source() {
image::Source::Uri { uri, mime_type: _ } => result.push(import_image_from_uri(uri)?),
image::Source::View { view, mime_type: _ } => {
result.push(import_image_from_view(view, buffers)?)
}
}
}
Ok(result)
}
fn import_image_from_uri(uri: &str) -> Result<SourceImage> {
let parsed_uri = uri::parse_uri(uri);
match parsed_uri.scheme {
uri::Scheme::Data if parsed_uri.data.is_some() => Ok(SourceImage::Embedded(decode_base64(
parsed_uri.data.unwrap(),
)?)),
uri::Scheme::None => Ok(SourceImage::External(uri)),
_ => Err(GltfMaterialError::UnsupportedURI(uri.into())),
}
}
fn import_image_from_view<'a>(view: View, buffers: &'a [Vec<u8>]) -> Result<SourceImage<'a>> {
let offset: usize = view.offset();
let length: usize = view.length();
let buf: &Vec<u8> = buffers
.get(view.buffer().index())
.ok_or(GltfMaterialError::InvalidIndex)?;
Ok(SourceImage::View(&buf[offset..offset + length]))
}
pub struct TextureContext<'a> {
pub resource_manager: &'a ResourceManager,
pub model_path: &'a Path,
pub search_options: &'a MaterialSearchOptions,
}
pub async fn import_textures<'a>(
gltf: &'a Document,
images: &[SourceImage<'a>],
context: TextureContext<'a>,
) -> Result<Vec<TextureResource>> {
let mut result: Vec<TextureResource> = Vec::with_capacity(gltf.textures().len());
for tex in gltf.textures() {
let sampler = tex.sampler();
let source = tex.source();
let image = images
.get(source.index())
.ok_or(GltfMaterialError::InvalidIndex)?;
match image {
SourceImage::Embedded(data) => result.push(import_embedded_texture(sampler, data)?),
SourceImage::View(data) => result.push(import_embedded_texture(sampler, data)?),
SourceImage::External(filename) => {
import_external_texture(filename, &context).await?;
} // result.push(import_external_texture(filename, &context).await?),
}
}
Ok(result)
}
fn import_embedded_texture(
sampler: gltf::texture::Sampler,
data: &[u8],
) -> Result<TextureResource> {
let mut options = TextureImportOptions::default();
if let Some(filter) = sampler.min_filter() {
options.set_minification_filter(convert_mini(filter));
}
if let Some(filter) = sampler.mag_filter() {
options.set_magnification_filter(convert_mag(filter));
}
options.set_s_wrap_mode(convert_wrap(sampler.wrap_s()));
options.set_t_wrap_mode(convert_wrap(sampler.wrap_t()));
let tex = Texture::load_from_memory(data, options)?;
Ok(Resource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
tex,
))
}
async fn import_external_texture(
filename: &str,
context: &TextureContext<'_>,
) -> Result<TextureResource> {
let path = search_for_path(filename, context)
.await
.ok_or_else(|| GltfMaterialError::TextureNotFound(filename.into()))?;
Ok(context.resource_manager.request(path))
}
async fn search_for_path(filename: &str, context: &TextureContext<'_>) -> Option<PathBuf> {
match context.search_options {
MaterialSearchOptions::MaterialsDirectory(ref directory) => Some(directory.join(filename)),
MaterialSearchOptions::RecursiveUp => {
let io = context.resource_manager.resource_io();
let mut texture_path = None;
let mut path: PathBuf = context.model_path.to_owned();
while let Some(parent) = path.parent() {
let candidate = parent.join(filename);
if io.exists(&candidate).await {
texture_path = Some(candidate);
break;
}
path.pop();
}
texture_path
}
MaterialSearchOptions::WorkingDirectory => {
let io = context.resource_manager.resource_io();
let mut texture_path = None;
let path = Path::new(".");
if let Ok(iter) = io.walk_directory(path, usize::MAX).await {
for dir in iter {
if io.is_dir(&dir).await {
let candidate = dir.join(filename);
if candidate.exists() {
texture_path = Some(candidate);
break;
}
}
}
}
texture_path
}
MaterialSearchOptions::UsePathDirectly => Some(filename.into()),
}
}
+771
View File
@@ -0,0 +1,771 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! [GltfLoader] enables the importing of *.gltf and *.glb files in the glTF format.
//! This requires the "gltf" feature.
use crate::asset::io::ResourceIo;
use crate::asset::loader;
use crate::asset::manager::ResourceManager;
use crate::asset::options;
use crate::asset::state::LoadError;
use crate::core::algebra::{Matrix4, Unit};
use crate::core::log::Log;
use crate::core::pool::Handle;
use crate::graph::NodeMapping;
use crate::graph::SceneGraph;
use crate::gui::core::io::FileError;
use crate::material::MaterialResource;
use crate::resource::model::{MaterialSearchOptions, Model, ModelImportOptions};
use crate::resource::texture::{TextureError, TextureResource};
use crate::scene::animation::{AnimationContainer, AnimationPlayerBuilder};
use crate::scene::base::BaseBuilder;
use crate::scene::graph::Graph;
use crate::scene::mesh::surface::{BlendShape, Surface, SurfaceResource};
use crate::scene::mesh::{Mesh, MeshBuilder};
use crate::scene::node::Node;
use crate::scene::pivot::PivotBuilder;
use crate::scene::transform::TransformBuilder;
use crate::scene::Scene;
use gltf::json;
use gltf::Document;
use gltf::Gltf;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uuid::Uuid;
mod animation;
mod iter;
pub mod material;
mod node_names;
mod simplify;
mod surface;
mod uri;
use animation::import_animations;
use fyrox_core::reflect::Reflect;
use fyrox_resource::untyped::ResourceKind;
use material::*;
pub use surface::SurfaceDataError;
use surface::{build_surface_data, BlendShapeInfoContainer, GeometryStatistics};
pub use uri::{parse_uri, Scheme, Uri};
type Result<T> = std::result::Result<T, GltfLoadError>;
const TARGET_NAMES_KEY: &str = "targetNames";
#[derive(Debug)]
#[allow(dead_code)]
enum GltfLoadError {
InvalidIndex,
InvalidPath,
UnsupportedURI(Box<str>),
MissingEmbeddedBin,
Gltf(gltf::Error),
Texture(TextureError),
File(FileError),
Base64(base64::DecodeError),
Load(LoadError),
Material(GltfMaterialError),
Surface(SurfaceDataError),
JSON(json::Error),
}
impl std::error::Error for GltfLoadError {}
impl Display for GltfLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GltfLoadError::InvalidIndex => f.write_str("Invalid index"),
GltfLoadError::InvalidPath => f.write_str("Invalid path"),
GltfLoadError::UnsupportedURI(uri) => write!(f, "Unsupported URL {uri:?}"),
GltfLoadError::MissingEmbeddedBin => f.write_str("Missing embedded bin"),
GltfLoadError::Gltf(error) => Display::fmt(error, f),
GltfLoadError::Texture(error) => Display::fmt(error, f),
GltfLoadError::File(error) => Display::fmt(error, f),
GltfLoadError::Base64(error) => Display::fmt(error, f),
GltfLoadError::Load(error) => Display::fmt(error, f),
GltfLoadError::Material(error) => Display::fmt(error, f),
GltfLoadError::Surface(error) => Display::fmt(error, f),
GltfLoadError::JSON(error) => Display::fmt(error, f),
}
}
}
impl From<json::Error> for GltfLoadError {
fn from(error: json::Error) -> Self {
GltfLoadError::JSON(error)
}
}
impl From<gltf::Error> for GltfLoadError {
fn from(error: gltf::Error) -> Self {
GltfLoadError::Gltf(error)
}
}
impl From<TextureError> for GltfLoadError {
fn from(error: TextureError) -> Self {
GltfLoadError::Texture(error)
}
}
impl From<FileError> for GltfLoadError {
fn from(error: FileError) -> Self {
GltfLoadError::File(error)
}
}
impl From<LoadError> for GltfLoadError {
fn from(error: LoadError) -> Self {
GltfLoadError::Load(error)
}
}
impl From<base64::DecodeError> for GltfLoadError {
fn from(error: base64::DecodeError) -> Self {
GltfLoadError::Base64(error)
}
}
impl From<GltfMaterialError> for GltfLoadError {
fn from(error: GltfMaterialError) -> Self {
GltfLoadError::Material(error)
}
}
impl From<SurfaceDataError> for GltfLoadError {
fn from(error: SurfaceDataError) -> Self {
GltfLoadError::Surface(error)
}
}
fn decode_base64(source: &str) -> Result<Vec<u8>> {
Ok(uri::decode_base64(source)?)
}
struct MeshData {
surfaces: Vec<Surface>,
blend_shapes: Vec<BlendShape>,
}
struct NodeFamily {
main_node: Handle<Node>,
bone_children: Vec<SkinNodePair>,
}
struct SkinNodePair {
skin_index: usize,
node: Handle<Node>,
}
type SkinData = Vec<SkinBone>;
#[derive(PartialEq, Debug, Clone)]
struct SkinBone {
pub node_index: usize,
pub inv_bind_pose: Matrix4<f32>,
}
impl From<(usize, Matrix4<f32>)> for SkinBone {
fn from(pair: (usize, Matrix4<f32>)) -> Self {
let (node_index, inv_bind_pose) = pair;
SkinBone {
node_index,
inv_bind_pose,
}
}
}
#[derive(Clone, Copy)]
struct SkinBonePair<'a> {
skin_index: usize,
bone: &'a SkinBone,
}
struct SkinBoneIter<'a> {
skin_index: usize,
bone_index: usize,
skin_list: &'a [SkinData],
}
impl<'a> SkinBoneIter<'a> {
fn new(skin_list: &'a [SkinData]) -> SkinBoneIter<'a> {
SkinBoneIter {
skin_index: 0,
bone_index: 0,
skin_list,
}
}
}
impl<'a> Iterator for SkinBoneIter<'a> {
type Item = SkinBonePair<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.skin_index >= self.skin_list.len() {
return None;
}
let skin: &SkinData = &self.skin_list[self.skin_index];
if self.bone_index >= skin.len() {
self.bone_index = 0;
self.skin_index += 1;
} else {
let bone = &skin[self.bone_index];
self.bone_index += 1;
return Some(SkinBonePair {
skin_index: self.skin_index,
bone,
});
}
}
}
}
struct ImportContext {
io: Arc<dyn ResourceIo>,
resource_manager: ResourceManager,
model_path: PathBuf,
search_options: MaterialSearchOptions,
}
impl ImportContext {
fn as_texture_context(&self) -> TextureContext {
TextureContext {
resource_manager: &self.resource_manager,
model_path: &self.model_path,
search_options: &self.search_options,
}
}
}
#[derive(Default)]
struct ImportResults {
buffers: Option<Vec<Vec<u8>>>,
textures: Option<Vec<TextureResource>>,
materials: Option<Vec<MaterialResource>>,
skins: Option<Vec<SkinData>>,
meshes: Option<Vec<MeshData>>,
families: Option<Vec<NodeFamily>>,
}
impl ImportResults {
fn get_buffer_data_access<'s>(
&'s self,
) -> impl Clone + Fn(gltf::Buffer<'_>) -> Option<&'s [u8]> {
|b| {
self.buffers
.as_ref()
.unwrap()
.get(b.index())
.map(Vec::as_slice)
}
}
}
/// This object performs the loading of files in glTF format with extension "gltf" or "glb".
pub struct GltfLoader {
/// ResourceManager is needed so that textures and mesh data can be loaded from additional resources.
/// The glTF format allows for other assets to be referenced by file path.
pub resource_manager: ResourceManager,
/// Import options control where this loader should search for additional resources.
pub default_import_options: ModelImportOptions,
}
impl loader::ResourceLoader for GltfLoader {
fn extensions(&self) -> &[&str] {
&["gltf", "glb"]
}
fn data_type_uuid(&self) -> Uuid {
<Model as Reflect>::type_info().type_uuid
}
fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> loader::BoxedLoaderFuture {
let resource_manager = self.resource_manager.clone();
let default_import_options = self.default_import_options.clone();
Box::pin(async move {
let import_options = options::try_get_import_settings(&path, io.as_ref())
.await
.unwrap_or(default_import_options);
let model = load(path, io, resource_manager, import_options)
.await
.map_err(LoadError::new)?;
Ok(loader::LoaderPayload::new(model))
})
}
fn try_load_import_settings(
&self,
resource_path: PathBuf,
io: Arc<dyn ResourceIo>,
) -> loader::BoxedImportOptionsLoaderFuture {
Box::pin(async move {
options::try_get_import_settings_opaque::<ModelImportOptions>(&resource_path, &*io)
.await
})
}
fn default_import_options(&self) -> Option<Box<dyn options::BaseImportOptions>> {
Some(Box::<ModelImportOptions>::default())
}
}
async fn load(
path: PathBuf,
io: Arc<dyn ResourceIo>,
resource_manager: ResourceManager,
options: ModelImportOptions,
) -> Result<Model> {
let mut scene = Scene::new();
let context = ImportContext {
io,
resource_manager,
model_path: path.clone(),
search_options: options.material_search_options,
};
let root_name = path
.file_name()
.ok_or(GltfLoadError::InvalidPath)?
.to_string_lossy();
let root = scene.graph.get_root();
scene.graph[root].set_name(root_name.clone());
import_from_path(&mut scene.graph, &context).await?;
node_names::resolve_name_conflicts(context.model_path.as_path(), &mut scene.graph);
Ok(Model::new(NodeMapping::UseNames, scene))
}
async fn import_from_path(graph: &mut Graph, context: &ImportContext) -> Result<()> {
let file: Vec<u8> = context.io.load_file(context.model_path.as_path()).await?;
import_from_slice(file.as_slice(), graph, context).await
}
async fn import_from_slice(slice: &[u8], graph: &mut Graph, context: &ImportContext) -> Result<()> {
let gltf: Gltf = Gltf::from_slice(slice)?;
let doc = gltf.document;
let data = gltf.blob;
let mut imports: ImportResults = ImportResults {
buffers: Some(import_buffers(&doc, data, context).await?),
..Default::default()
};
let buffers: &[Vec<u8>] = imports.buffers.as_ref().unwrap().as_slice();
let images: Vec<SourceImage> = import_images(&doc, buffers)?;
imports.textures =
Some(import_textures(&doc, images.as_slice(), context.as_texture_context()).await?);
let textures = imports.textures.as_ref().unwrap().as_slice();
imports.materials = Some(import_materials(&doc, textures).await?);
let materials = imports.materials.as_ref().unwrap().as_slice();
imports.skins = Some(import_skins(&doc, &imports)?);
imports.meshes = Some(import_meshes(
&doc,
&context.model_path,
materials,
buffers,
)?);
imports.families = Some(import_nodes(&doc, graph, &imports)?);
link_child_nodes(&doc, graph, &imports)?;
let node_handles: Vec<Handle<Node>> = imports
.families
.as_ref()
.unwrap()
.iter()
.map(|f| f.main_node)
.collect();
let animations = import_animations(&doc, &node_handles, graph, buffers);
if !animations.is_empty() {
let mut anim_con = AnimationContainer::new();
for animation in animations {
anim_con.add(animation);
}
AnimationPlayerBuilder::new(BaseBuilder::new().with_name("AnimationPlayer"))
.with_animations(anim_con)
.build(graph);
}
Ok(())
}
async fn import_buffers(
gltf: &Document,
mut data_chunk: Option<Vec<u8>>,
context: &ImportContext,
) -> Result<Vec<Vec<u8>>> {
let mut result: Vec<Vec<u8>> = Vec::with_capacity(gltf.buffers().len());
for buf in gltf.buffers() {
match buf.source() {
gltf::buffer::Source::Bin => match data_chunk.take() {
Some(data) => result.push(data),
None => {
return Err(GltfLoadError::MissingEmbeddedBin);
}
},
gltf::buffer::Source::Uri(uri) => result.push(load_bin_from_uri(uri, context).await?),
}
}
Ok(result)
}
async fn load_bin_from_uri(uri: &str, context: &ImportContext) -> Result<Vec<u8>> {
let parsed_uri = uri::parse_uri(uri);
match parsed_uri.scheme {
uri::Scheme::Data if parsed_uri.data.is_some() => {
Ok(decode_base64(parsed_uri.data.unwrap())?)
}
uri::Scheme::None => load_external_bin(uri, context).await,
_ => Err(GltfLoadError::UnsupportedURI(uri.into())),
}
}
async fn load_external_bin(path: &str, context: &ImportContext) -> Result<Vec<u8>> {
let parent = context
.model_path
.parent()
.ok_or(GltfLoadError::InvalidPath)?
.to_owned();
let path = parent.join(path);
Ok(context.io.load_file(&path).await?)
}
fn import_meshes(
gltf: &Document,
path: &Path,
mats: &[MaterialResource],
bufs: &[Vec<u8>],
) -> Result<Vec<MeshData>> {
let mut result: Vec<MeshData> = Vec::with_capacity(gltf.nodes().len());
let mut stats = GeometryStatistics::default();
for node in gltf.nodes() {
if let Some(mesh) = node.mesh() {
result.push(import_mesh(mesh, mats, bufs, &mut stats)?);
}
}
if cfg!(feature = "mesh_analysis") {
if stats.repeated_index_count > 0 {
Log::err(format!(
"{}: Model has triangles with repeated vertices: {}",
path.to_string_lossy(),
stats.repeated_index_count
));
}
let min_length = stats.min_edge_length();
if min_length == 0.0 {
Log::err(format!(
"{}: Mesh has a triangle with a zero-length edge!",
path.to_string_lossy()
));
} else if min_length <= f32::EPSILON {
Log::err(format!(
"{}: Mesh has a triangle with edge length: {}",
path.to_string_lossy(),
min_length
));
} else {
Log::info(format!(
"{}: Smallest triangle edge: {}",
path.to_string_lossy(),
min_length
));
}
}
Ok(result)
}
fn import_mesh(
mesh: gltf::Mesh,
mats: &[MaterialResource],
bufs: &[Vec<u8>],
stats: &mut GeometryStatistics,
) -> Result<MeshData> {
let morph_info = import_morph_info(&mesh)?;
let mut surfs: Vec<Surface> = Vec::with_capacity(mesh.primitives().len());
let mut blend_shapes: Option<Vec<BlendShape>> = None;
for prim in mesh.primitives() {
if let Some((surf, shapes)) = import_surface(prim, &morph_info, mats, bufs, stats)? {
surfs.push(surf);
blend_shapes.get_or_insert(shapes);
}
}
Ok(MeshData {
surfaces: surfs,
blend_shapes: blend_shapes.unwrap_or_default(),
})
}
fn import_morph_info(mesh: &gltf::Mesh) -> Result<BlendShapeInfoContainer> {
let weights: &[f32] = mesh.weights().unwrap_or_default();
let weights: Vec<f32> = weights.iter().map(|w| w * 100.0).collect();
let extras = mesh.extras();
let names = if let Some(extras) = extras {
let extras: json::Value = json::deserialize::from_str(extras.get())?;
match extras {
json::Value::Object(map) => {
if let Some(names) = map.get(TARGET_NAMES_KEY) {
match names {
json::Value::Array(names) => {
values_to_strings(names.as_slice()).unwrap_or_default()
}
_ => Vec::default(),
}
} else {
Vec::default()
}
}
_ => Vec::default(),
}
} else {
Vec::default()
};
if extras.is_some() && names.is_empty() {
Log::warn(format!(
"glTF: Unable to extract blend shape names from JSON: {}",
extras.as_ref().unwrap().get()
));
}
Ok(BlendShapeInfoContainer::new(names, weights))
}
fn values_to_strings(values: &[json::Value]) -> Option<Vec<String>> {
let mut result: Vec<String> = Vec::with_capacity(values.len());
for v in values {
if let json::Value::String(str) = v {
result.push(str.clone());
} else {
return None;
}
}
Some(result)
}
fn import_surface(
prim: gltf::Primitive,
morph_info: &BlendShapeInfoContainer,
mats: &[MaterialResource],
bufs: &[Vec<u8>],
stats: &mut GeometryStatistics,
) -> Result<Option<(Surface, Vec<BlendShape>)>> {
if let Some(data) = build_surface_data(&prim, morph_info, bufs, stats)? {
let mut blend_shapes = Vec::new();
if let Some(shape_con) = data.blend_shapes_container.as_ref() {
blend_shapes.clone_from(&shape_con.blend_shapes)
}
let mut surf = Surface::new(SurfaceResource::new_ok(
Uuid::new_v4(),
ResourceKind::External,
data,
));
if let Some(mat_index) = prim.material().index() {
surf.set_material(
mats.get(mat_index)
.ok_or(GltfLoadError::InvalidIndex)?
.clone(),
);
Ok(Some((surf, blend_shapes)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn import_nodes(
doc: &gltf::Document,
graph: &mut Graph,
imports: &ImportResults,
) -> Result<Vec<NodeFamily>> {
let skins: &[SkinData] = imports.skins.as_ref().unwrap().as_slice();
let mut result: Vec<NodeFamily> = Vec::with_capacity(doc.nodes().len());
for node in doc.nodes() {
result.push(build_node_family(&node, skins, graph, imports)?);
}
for node in doc.nodes() {
let family: &NodeFamily = result
.get(node.index())
.ok_or(GltfLoadError::InvalidIndex)?;
if let Some(mesh) = graph[family.main_node].cast_mut::<Mesh>() {
assign_bones_to_surfaces(node, mesh, result.as_slice())?;
}
}
Ok(result)
}
fn build_node_family(
node: &gltf::Node,
skins: &[SkinData],
graph: &mut Graph,
imports: &ImportResults,
) -> Result<NodeFamily> {
let node_index = node.index();
let skin_iter = SkinBoneIter::new(skins).filter(move |sb| sb.bone.node_index == node_index);
let mut bones: Vec<SkinBonePair> = Vec::new();
let mut new_handle: Option<Handle<Node>> = None;
let mut bone_children: Vec<SkinNodePair> = Vec::new();
let name = node.name().unwrap_or("");
for pair in skin_iter {
if bones.is_empty() {
// Our first bone. Set the inv_bind_pose of the main node.
// We only create children if we later find an inv_bind_pose that does not match this.
let new_node = import_node(node, pair.bone.inv_bind_pose, imports)?;
new_handle = Some(graph.add_node(new_node));
// Record that we have seen this inv_bind_pose.
bones.push(pair);
} else if let Some(p) = bones.iter().find(|p| p.bone == pair.bone) {
// We have a previously existing bone with the exact same inv_bind_pose.
// Do not record having seen this inv_bind_pose.
// Let the already recorded inv_bind_pose stand in for all bones with this inv_bind_pose.
let prev_skin_index = p.skin_index;
if let Some(prev_pair) = bone_children
.iter()
.find(|b| b.skin_index == prev_skin_index)
{
// We have a child for the skin of the previously existing bone. Re-use that child for this skin.
bone_children.push(SkinNodePair {
skin_index: pair.skin_index,
node: prev_pair.node,
});
}
// Otherwise, the previously existing bone must be using the main node, so do nothing.
} else {
// We have a never-seen-before inv_bind_pose, so create a child for that inv_bind_pose.
let skin_index = pair.skin_index;
let base_builder = BaseBuilder::new()
.with_name(format!("{name}:{skin_index}"))
.with_inv_bind_pose_transform(pair.bone.inv_bind_pose);
let handle: Handle<Node> = graph.add_node(PivotBuilder::new(base_builder).build_node());
bone_children.push(SkinNodePair {
skin_index,
node: handle,
});
graph.link_nodes(handle, new_handle.unwrap());
// Record that we have seen this inv_bind_pose.
bones.push(pair);
}
}
if let Some(handle) = new_handle {
Ok(NodeFamily {
main_node: handle,
bone_children,
})
} else {
Ok(NodeFamily {
main_node: graph.add_node(import_node(node, Matrix4::identity(), imports)?),
bone_children: Vec::new(),
})
}
}
fn assign_bones_to_surfaces(
node: gltf::Node,
mesh: &mut Mesh,
families: &[NodeFamily],
) -> Result<()> {
if let Some(skin) = node.skin() {
let skin_index = skin.index();
let mut bones: Vec<Handle<Node>> = Vec::with_capacity(skin.joints().len());
for joint in skin.joints() {
let joint_family = families
.get(joint.index())
.ok_or(GltfLoadError::InvalidIndex)?;
let bone_children = &joint_family.bone_children;
let handle =
if let Some(pair) = bone_children.iter().find(|p| p.skin_index == skin_index) {
pair.node
} else {
joint_family.main_node
};
bones.push(handle);
}
for surf in mesh.surfaces_mut() {
surf.bones.set_value_and_mark_modified(bones.clone());
}
}
Ok(())
}
fn import_node(
node: &gltf::Node,
inv_bind_pose: Matrix4<f32>,
imports: &ImportResults,
) -> Result<Node> {
let meshes: &[MeshData] = imports.meshes.as_ref().unwrap().as_slice();
let trans = node.transform().decomposed();
let trans_builder: TransformBuilder = TransformBuilder::new()
.with_local_position(trans.0.into())
.with_local_rotation(Unit::new_normalize(trans.1.into()))
.with_local_scale(trans.2.into());
let name = node.name().unwrap_or("");
let base_builder = BaseBuilder::new()
.with_name(name)
.with_local_transform(trans_builder.build())
.with_inv_bind_pose_transform(inv_bind_pose);
if let Some(mesh) = node.mesh() {
let mut mesh_builder = MeshBuilder::new(base_builder);
let mesh = meshes
.get(mesh.index())
.ok_or(GltfLoadError::InvalidIndex)?;
mesh_builder = mesh_builder.with_blend_shapes(mesh.blend_shapes.clone());
mesh_builder = mesh_builder.with_surfaces(mesh.surfaces.clone());
Ok(mesh_builder.build_node())
} else {
Ok(PivotBuilder::new(base_builder).build_node())
}
}
fn link_child_nodes(doc: &Document, graph: &mut Graph, imports: &ImportResults) -> Result<()> {
let families: &[NodeFamily] = imports.families.as_ref().unwrap().as_slice();
for node in doc.nodes() {
let parent_family = families
.get(node.index())
.ok_or(GltfLoadError::InvalidIndex)?;
for child in node.children() {
let child_family = families
.get(child.index())
.ok_or(GltfLoadError::InvalidIndex)?;
graph.link_nodes(child_family.main_node, parent_family.main_node);
}
}
Ok(())
}
fn import_skins(doc: &gltf::Document, imports: &ImportResults) -> Result<Vec<SkinData>> {
let mut result: Vec<SkinData> = Vec::with_capacity(doc.skins().len());
for skin in doc.skins() {
let bone_node_indices = skin.joints().map(|n| n.index());
let bone_pairs = {
let reader = skin.reader(imports.get_buffer_data_access());
if let Some(iter) = reader.read_inverse_bind_matrices() {
bone_node_indices
.zip(iter.map(Matrix4::from))
.map(SkinBone::from)
.collect()
} else {
bone_node_indices
.zip(std::iter::repeat(Matrix4::identity()))
.map(SkinBone::from)
.collect()
}
};
result.push(bone_pairs);
}
Ok(result)
}
+203
View File
@@ -0,0 +1,203 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::core::log::Log;
use crate::core::pool::Handle;
use crate::core::NameProvider;
use crate::fxhash::{FxHashMap, FxHashSet};
use crate::graph::SceneGraph;
use crate::scene::graph::Graph;
use crate::scene::node::Node;
use std::cell::RefCell;
use std::path::Path;
#[derive(Debug)]
struct NodeName {
handle: Handle<Node>,
name: RefCell<String>,
}
/// Attempt to remove duplicate names by renaming the nodes of the graph
/// without depending on the order of sibling nodes.
///
/// If a parent node has the same name as its child node, the child node
/// is renamed to "parent_name+1". Grandchildren are renamed to "parent_name+2"
/// and so on.
///
/// If two nodes have the same name, but their parents have different names,
/// the nodes are renamed to "node_name+parentA" and "node_name+parentB".
///
/// If the nodes have parents with the same names, then grandparent names may
/// be used if the grandparents have different names. If the whole ancestry
/// of the nodes is searched without finding ancestors with distinct names
/// then the nodes are not renamed.
///
/// If after the whole procedure there is found to still be nodes with duplicate names,
/// an error is logged to alert the user to the problem.
pub fn resolve_name_conflicts(path: &Path, graph: &mut Graph) {
let node_sets: Vec<Vec<Handle<Node>>> = build_node_sets_from_graph(graph);
for nodes in node_sets {
if nodes.len() > 1 {
resolve_conflict(nodes, graph);
}
}
// Check if conflicts have actually been resolved.
let mut name_set: FxHashSet<&str> = FxHashSet::default();
for node in graph.linear_iter() {
if !name_set.insert(node.name()) {
Log::err(format!(
"A node with existing name {} was found during the load of {} resource! \
Do **NOT IGNORE** this message, please fix names in your model, otherwise \
engine won't be able to correctly restore data from your resource!",
node.name(),
path.display()
));
}
}
}
fn build_node_sets_from_graph(graph: &Graph) -> Vec<Vec<Handle<Node>>> {
let mut name_map: FxHashMap<&str, Vec<Handle<Node>>> = FxHashMap::default();
for (handle, node) in graph.pair_iter() {
let name = node.name();
let list = name_map
.entry(name)
.or_insert_with(|| Vec::with_capacity(1));
list.push(handle);
}
name_map.into_values().collect()
}
fn build_node_sets_from_list(nodes: &[NodeName]) -> Vec<Vec<&NodeName>> {
let mut name_map: FxHashMap<&str, Vec<&NodeName>> = FxHashMap::default();
let names: Vec<_> = nodes.iter().map(|n| n.name.borrow()).collect();
for (node, name) in nodes.iter().zip(names.iter()) {
let list = name_map
.entry(name.as_str())
.or_insert_with(|| Vec::with_capacity(1));
list.push(node);
}
name_map.into_values().collect()
}
fn resolve_conflict(handles: Vec<Handle<Node>>, graph: &mut Graph) {
// Resolve cases where parents have the same names as children.
let mut node_names = build_node_name_list(handles.as_slice(), graph);
for NodeName { handle, name } in node_names.iter_mut() {
let d = count_ancestor_depth(*handle, handles.as_slice(), graph);
if d > 0 {
name.borrow_mut().push('+');
name.borrow_mut().push_str(d.to_string().as_str());
}
}
// Build lists of nodes which still have duplicate names
let node_sets = build_node_sets_from_list(&node_names);
// Add ancestor names if that will eliminate duplicates.
for set in node_sets {
if set.len() > 1 {
if let Some(names) = find_distinct_ancestor_names(set.as_slice(), graph) {
for (node, name) in set.iter().zip(names.iter()) {
node.name.borrow_mut().push('+');
node.name.borrow_mut().push_str(name);
}
}
}
}
for node in node_names {
graph[node.handle].set_name(node.name.borrow().as_str());
}
}
fn build_node_name_list(handles: &[Handle<Node>], graph: &Graph) -> Vec<NodeName> {
handles
.iter()
.map(|h| NodeName {
handle: *h,
name: RefCell::new(graph[*h].name().to_owned()),
})
.collect()
}
fn find_distinct_ancestor_names<'a>(
node_names: &[&NodeName],
graph: &'a Graph,
) -> Option<Vec<&'a str>> {
for i in 1.. {
if let Some(names) = get_ancestor_names(node_names, i, graph) {
if !contains_duplicate_name(names.as_slice()) {
return Some(names);
}
} else {
return None;
}
}
None
}
fn get_ancestor_names<'a>(
node_names: &[&NodeName],
depth: usize,
graph: &'a Graph,
) -> Option<Vec<&'a str>> {
let mut result: Vec<&'a str> = Vec::with_capacity(node_names.len());
for n in node_names {
if let Some(n) = get_ancestor_name(n.handle, depth, graph) {
result.push(n);
} else {
return None;
}
}
Some(result)
}
fn get_ancestor_name(mut handle: Handle<Node>, depth: usize, graph: &Graph) -> Option<&str> {
for _ in 0..depth {
if let Ok(n) = graph.try_get_node(handle) {
handle = n.parent();
} else {
return None;
}
}
graph.try_get_node(handle).ok().map(Node::name)
}
fn contains_duplicate_name(names: &[&str]) -> bool {
let mut iter = names.iter();
while let Some(n) = iter.next() {
let mut rest = iter.clone();
if rest.any(|x| *n == *x) {
return true;
}
}
false
}
fn count_ancestor_depth(mut handle: Handle<Node>, list: &[Handle<Node>], graph: &Graph) -> usize {
let mut count: usize = 0;
while let Ok(node) = graph.try_get_node(handle) {
handle = node.parent();
if list.contains(&handle) {
count += 1;
} else {
break;
}
}
count
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::fmt::Debug;
pub trait CurvePoint {
fn x(&self) -> f32;
fn y(&self) -> f32;
}
pub fn simplify<P: CurvePoint + Clone + Debug>(
points: &[P],
epsilon: f32,
max_step: f32,
) -> Vec<P> {
find_important_points(points, epsilon, max_step)
.into_iter()
.map(|i| points[i].clone())
.collect()
}
pub fn find_important_points<P: CurvePoint + Debug>(
points: &[P],
epsilon: f32,
max_step: f32,
) -> Vec<usize> {
if points.is_empty() {
return Vec::new();
}
let mut keep_flags: Vec<bool> = Vec::new();
keep_flags.resize(points.len(), false);
let end = keep_flags.len() - 1;
keep_flags[0] = true;
keep_flags[end] = true;
find_points_in_span(points, keep_flags.as_mut_slice(), 0, end, epsilon);
if max_step.is_finite() {
limit_step_size(points, keep_flags.as_mut_slice(), max_step);
}
let mut result: Vec<usize> = Vec::new();
for (i, k) in keep_flags.into_iter().enumerate() {
if k {
result.push(i)
}
}
if result.len() == 2 && f32::abs(points[result[0]].y() - points[result[1]].y()) < epsilon {
result.pop();
}
result
}
fn limit_step_size<P: CurvePoint>(points: &[P], keep_flags: &mut [bool], max_step: f32) {
let end = points.len() - 1;
let mut i: usize = 1;
while i < end {
if keep_flags[i] {
i += 1;
} else {
let next = find_step(i - 1, points, keep_flags, max_step);
keep_flags[next] = true;
i = usize::max(next + 1, i + 1);
}
}
}
fn find_step<P: CurvePoint>(
start: usize,
points: &[P],
keep_flags: &mut [bool],
max_step: f32,
) -> usize {
let start_y = points[start].y();
for i in start + 1..points.len() {
let step = f32::abs(points[i].y() - start_y);
if step > max_step {
return usize::max(i - 1, start + 1);
} else if keep_flags[i] {
return i;
}
}
points.len() - 1
}
#[allow(clippy::needless_range_loop)]
fn find_points_in_span<P: CurvePoint + Debug>(
points: &[P],
keep_flags: &mut [bool],
start: usize,
end: usize,
epsilon: f32,
) {
if end <= start + 1 {
return;
}
let x0 = points[start].x();
let y0 = points[start].y();
let slope = (points[end].y() - y0) / (points[end].x() - x0);
let mut far_point_index: usize = 0;
let mut far_point_dist: f32 = 0.0;
for i in start + 1..end {
let (x, y) = (points[i].x(), points[i].y());
let y_line: f32 = y0 + slope * (x - x0);
let dist: f32 = (y - y_line).abs();
if far_point_dist < dist {
far_point_dist = dist;
far_point_index = i;
}
}
if far_point_index == 0 || far_point_dist < epsilon {
return;
}
keep_flags[far_point_index] = true;
find_points_in_span(points, keep_flags, start, far_point_index, epsilon);
find_points_in_span(points, keep_flags, far_point_index, end, epsilon);
}
#[cfg(test)]
mod tests {
use super::*;
type Point = (f32, f32);
impl CurvePoint for Point {
fn x(&self) -> f32 {
self.0
}
fn y(&self) -> f32 {
self.1
}
}
#[test]
fn empty() {
let points: Vec<Point> = Vec::new();
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result.len(), 0);
}
#[test]
fn size_1() {
let points: Vec<Point> = vec![(0.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0]);
}
#[test]
fn size_2() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 1]);
}
#[test]
fn size_3() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 1, 2]);
}
#[test]
fn size_4() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 1, 2, 3]);
}
#[test]
fn size_5() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0), (3.0, 0.0), (4.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 1, 2, 3, 4]);
}
#[test]
fn irregular_x() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, -1.0), (6.0, 0.0), (10.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 1, 2, 3, 4]);
}
#[test]
fn irregular_x_remove_2() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (4.0, 4.0), (6.0, 2.0), (10.0, -2.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 2, 4]);
}
#[test]
fn remove_middle() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 2]);
}
#[test]
fn remove_all_but_one() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 0.00001), (2.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0]);
}
#[test]
fn remove_2() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, f32::INFINITY);
assert_eq!(result, vec![0, 2, 4]);
}
#[test]
fn small_step_size() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, 0.5);
assert_eq!(result, vec![0, 1, 2, 3, 4]);
}
#[test]
fn large_step_size() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.0), (4.0, 0.0)];
let result = find_important_points(points.as_slice(), 0.001, 2.0);
assert_eq!(result, vec![0, 2, 4]);
}
#[test]
fn mid_step_size() {
let points: Vec<Point> = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 1.5), (4.0, 1.0)];
let result = find_important_points(points.as_slice(), 0.001, 1.0);
assert_eq!(result, vec![0, 1, 2, 4]);
}
}

Some files were not shown because too many files have changed in this diff Show More