chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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:?}"))
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+282
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
"#,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
"#,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
"#,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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!(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
¶ms,
|
||||
&[ResourceBindGroup {
|
||||
bindings: &resource_bindings,
|
||||
}],
|
||||
element_range,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(statistics)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
"#,
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user