Compare commits

..

10 commits

6 changed files with 240 additions and 91 deletions

66
Cargo.lock generated
View file

@ -434,6 +434,51 @@ dependencies = [
"bevy_internal", "bevy_internal",
] ]
[[package]]
name = "bevy-inspector-egui"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4971e763f289921fd4616418628458bec26a6fc13fe4299c0e4066f39d7ceaa2"
dependencies = [
"bevy-inspector-egui-derive",
"bevy_app",
"bevy_asset",
"bevy_color",
"bevy_core_pipeline",
"bevy_ecs",
"bevy_egui",
"bevy_image",
"bevy_log",
"bevy_math",
"bevy_pbr",
"bevy_platform",
"bevy_reflect",
"bevy_render",
"bevy_state",
"bevy_time",
"bevy_utils",
"bevy_window",
"bytemuck",
"disqualified",
"egui",
"fuzzy-matcher",
"image",
"smallvec",
"uuid",
"winit",
]
[[package]]
name = "bevy-inspector-egui-derive"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2656316165dbe2af6b3acaa763332f5dbdd12f809d59f5bf4304e0642a8005c9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "bevy_a11y" name = "bevy_a11y"
version = "0.16.0" version = "0.16.0"
@ -1014,6 +1059,16 @@ dependencies = [
"glam", "glam",
] ]
[[package]]
name = "bevy_panorbit_camera"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6e15e297754d0bcb7665620c390c4f05665d4ac4ac91b4b5d3c66b9fe1f0e6"
dependencies = [
"bevy",
"bevy_egui",
]
[[package]] [[package]]
name = "bevy_pbr" name = "bevy_pbr"
version = "0.16.0" version = "0.16.0"
@ -1471,7 +1526,9 @@ name = "bevyart"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy-inspector-egui",
"bevy_egui", "bevy_egui",
"bevy_panorbit_camera",
] ]
[[package]] [[package]]
@ -2507,6 +2564,15 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"

View file

@ -5,7 +5,9 @@ edition = "2024"
[dependencies] [dependencies]
bevy = { version = "0.16.0", features = ["shader_format_wesl"] } bevy = { version = "0.16.0", features = ["shader_format_wesl"] }
bevy-inspector-egui = "0.31.0"
bevy_egui = "0.34.1" bevy_egui = "0.34.1"
bevy_panorbit_camera = { version = "0.26.0", features = ["bevy_egui"] }
# Enable a small amount of optimization in the dev profile. # Enable a small amount of optimization in the dev profile.
[profile.dev] [profile.dev]

View file

@ -7,18 +7,24 @@
struct VoluMaterial { struct VoluMaterial {
mesh_translation: vec3<f32>, mesh_translation: vec3<f32>,
sphere_radius: f32,
color: vec4<f32>,
model_inverse: mat4x4<f32>, model_inverse: mat4x4<f32>,
power: f32, power: f32,
iterations: i32, iterations: i32,
max_distance: f32,
max_steps: i32,
ray_epsilon: f32,
normal_epsilon: f32,
scale: f32,
} }
@group(2) @binding(100) @group(2) @binding(100)
var<storage, read> volu_material: VoluMaterial; var<storage, read> volu_material: VoluMaterial;
fn mandelbulb_sdf(p: vec3<f32>) -> f32 { fn mandelbulb_sdf(p: vec3<f32>) -> f32 {
var z = p; let scale = volu_material.scale;
let p_scaled = p / scale;
var z = p_scaled;
var dr = 1.0; var dr = 1.0;
var r = 0.0; var r = 0.0;
let power = volu_material.power; let power = volu_material.power;
@ -42,14 +48,14 @@ fn mandelbulb_sdf(p: vec3<f32>) -> f32 {
sin(new_theta) * cos(new_phi), sin(new_theta) * cos(new_phi),
sin(new_theta) * sin(new_phi), sin(new_theta) * sin(new_phi),
cos(new_theta) cos(new_theta)
) + p; ) + p_scaled;
} }
return 0.5 * log(max(r, 0.001)) * r / dr; return 0.5 * log(max(r, 0.001)) * r / dr * scale;
} }
fn mandelbulb_normal(p: vec3<f32>) -> vec3<f32> { fn mandelbulb_normal(p: vec3<f32>) -> vec3<f32> {
let e = 0.001; let e = volu_material.normal_epsilon;
let dx = mandelbulb_sdf(p + vec3<f32>(e, 0.0, 0.0)) - mandelbulb_sdf(p - vec3<f32>(e, 0.0, 0.0)); let dx = mandelbulb_sdf(p + vec3<f32>(e, 0.0, 0.0)) - mandelbulb_sdf(p - vec3<f32>(e, 0.0, 0.0));
let dy = mandelbulb_sdf(p + vec3<f32>(0.0, e, 0.0)) - mandelbulb_sdf(p - vec3<f32>(0.0, e, 0.0)); let dy = mandelbulb_sdf(p + vec3<f32>(0.0, e, 0.0)) - mandelbulb_sdf(p - vec3<f32>(0.0, e, 0.0));
let dz = mandelbulb_sdf(p + vec3<f32>(0.0, 0.0, e)) - mandelbulb_sdf(p - vec3<f32>(0.0, 0.0, e)); let dz = mandelbulb_sdf(p + vec3<f32>(0.0, 0.0, e)) - mandelbulb_sdf(p - vec3<f32>(0.0, 0.0, e));
@ -58,9 +64,9 @@ fn mandelbulb_normal(p: vec3<f32>) -> vec3<f32> {
fn raymarch(ray_origin: vec3<f32>, ray_direction: vec3<f32>) -> f32 { fn raymarch(ray_origin: vec3<f32>, ray_direction: vec3<f32>) -> f32 {
var t = 0.0; var t = 0.0;
let max_distance = 100.0; let max_distance = volu_material.max_distance;
let max_steps = 128; let max_steps = volu_material.max_steps;
let epsilon = 0.001; let epsilon = volu_material.ray_epsilon;
for (var i = 0; i < max_steps; i++) { for (var i = 0; i < max_steps; i++) {
let current_pos = ray_origin + t * ray_direction; let current_pos = ray_origin + t * ray_direction;
@ -87,7 +93,7 @@ fn fragment(
) -> FragmentOutput { ) -> FragmentOutput {
var in = vertex_output; var in = vertex_output;
let ray_origin = view.world_position; let ray_origin = in.world_position.xyz;
let ray_direction = normalize(in.world_position.xyz - view.world_position); let ray_direction = normalize(in.world_position.xyz - view.world_position);
let local_origin = (volu_material.model_inverse * vec4<f32>(ray_origin, 1.0)).xyz; let local_origin = (volu_material.model_inverse * vec4<f32>(ray_origin, 1.0)).xyz;
@ -98,7 +104,7 @@ fn fragment(
var out: FragmentOutput; var out: FragmentOutput;
if t > 0.0 { if t > 0.0 {
let local_hit_point = ray_origin + t * ray_direction; let local_hit_point = local_origin + t * local_direction;
let local_normal = mandelbulb_normal(local_hit_point); let local_normal = mandelbulb_normal(local_hit_point);
let world_hit_point = (transpose(volu_material.model_inverse) * vec4<f32>(local_hit_point, 1.0)).xyz; let world_hit_point = (transpose(volu_material.model_inverse) * vec4<f32>(local_hit_point, 1.0)).xyz;

14
src/camera.rs Normal file
View file

@ -0,0 +1,14 @@
use bevy::prelude::*;
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
pub fn plugin(app: &mut App) {
app.add_plugins(PanOrbitCameraPlugin)
.add_systems(Startup, setup);
}
fn setup(mut commands: Commands) {
commands.spawn((
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
PanOrbitCamera::default(),
));
}

View file

@ -1,50 +1,44 @@
use bevy::{ use bevy::{color::palettes::css::RED, prelude::*, render::storage::ShaderStorageBuffer};
color::palettes::css::RED, use bevy_egui::EguiPlugin;
pbr::{ExtendedMaterial, MaterialExtension}, use mandelbulb::{MandelbulbExtension, MandelbulbMaterial, MandelbulbStorage};
prelude::*,
render::{render_resource::*, storage::ShaderStorageBuffer},
};
const SHADER_ASSET_PATH: &str = "shaders/material.wgsl"; mod camera;
mod mandelbulb;
type VoluMaterial = ExtendedMaterial<StandardMaterial, VoluExtension>;
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins((
.add_plugins(MaterialPlugin::<VoluMaterial>::default()) DefaultPlugins,
EguiPlugin {
enable_multipass_for_primary_context: false,
},
))
.add_plugins((camera::plugin, mandelbulb::plugin))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, (rotate_things, update_volu_material)) .add_systems(Update, (rotate_things,))
.run(); .run();
} }
fn setup( fn setup(
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<VoluMaterial>>, mut materials: ResMut<Assets<MandelbulbMaterial>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>, mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
) { ) {
let transform = Transform::from_xyz(0.0, 0.0, 0.0); let transform = Transform::from_xyz(0.0, 0.0, 0.0);
let storage = buffers.add(ShaderStorageBuffer::from(VoluStorage { let storage = buffers.add(ShaderStorageBuffer::from(MandelbulbStorage::default()));
mesh_translation: transform.translation,
sphere_radius: 1.0,
color: Vec4::default(),
model_inverse: transform.compute_matrix().inverse(),
power: 8.0,
iterations: 32,
}));
commands.spawn(( commands.spawn((
Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
MeshMaterial3d(materials.add(ExtendedMaterial { MeshMaterial3d(materials.add(MandelbulbExtension::create_material(
base: StandardMaterial { StandardMaterial {
base_color: RED.into(), base_color: RED.into(),
alpha_mode: AlphaMode::Blend, alpha_mode: AlphaMode::Blend,
..default() ..default()
}, },
extension: VoluExtension { storage }, storage,
})), ))),
transform, transform,
Rotate, Rotate,
)); ));
@ -54,11 +48,6 @@ fn setup(
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y), Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
Rotate, Rotate,
)); ));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
} }
#[derive(Component)] #[derive(Component)]
@ -69,53 +58,3 @@ fn rotate_things(mut q: Query<&mut Transform, With<Rotate>>, time: Res<Time>) {
t.rotate_y(time.delta_secs()); t.rotate_y(time.delta_secs());
} }
} }
fn update_volu_material(
q: Query<(&Transform, &MeshMaterial3d<VoluMaterial>), Changed<Transform>>,
mut volu_materials: ResMut<Assets<VoluMaterial>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
time: Res<Time>,
) {
for (transform, material) in q.iter() {
let volu_material = volu_materials.get_mut(material.0.id()).unwrap();
let buffer = buffers
.get_mut(volu_material.extension.storage.id())
.unwrap();
let elapsed = time.elapsed_secs_wrapped();
let volu_storage = VoluStorage {
mesh_translation: transform.translation,
sphere_radius: (elapsed.sin() + 3.) / 4.,
color: vec4((elapsed.sin() + 1.) / 2., 0.0, 0.0, 1.0),
model_inverse: transform.compute_matrix(),
power: 8.0,
iterations: 32,
};
buffer.set_data(volu_storage);
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
struct VoluExtension {
#[storage(100, read_only)]
storage: Handle<ShaderStorageBuffer>,
}
impl MaterialExtension for VoluExtension {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}
#[derive(ShaderType, Clone, Default, Debug)]
pub struct VoluStorage {
mesh_translation: Vec3,
sphere_radius: f32,
color: Vec4,
model_inverse: Mat4,
power: f32,
iterations: i32,
}

122
src/mandelbulb.rs Normal file
View file

@ -0,0 +1,122 @@
use bevy::{
pbr::{ExtendedMaterial, MaterialExtension},
prelude::*,
render::{
render_resource::{AsBindGroup, ShaderRef, ShaderType},
storage::ShaderStorageBuffer,
},
};
use bevy_inspector_egui::quick::ResourceInspectorPlugin;
pub type MandelbulbMaterial = ExtendedMaterial<StandardMaterial, MandelbulbExtension>;
const SHADER_ASSET_PATH: &str = "shaders/mandelbulb.wgsl";
pub fn plugin(app: &mut App) {
app.add_plugins(MaterialPlugin::<MandelbulbMaterial>::default())
.init_resource::<MandelbulbSettings>()
.register_type::<MandelbulbSettings>()
.add_plugins(ResourceInspectorPlugin::<MandelbulbSettings>::new())
.add_systems(Update, update_volu_material);
}
#[derive(Resource, Debug, Reflect)]
struct MandelbulbSettings {
power: f32,
iterations: i32,
max_distance: f32,
max_steps: i32,
ray_epsilon: f32,
normal_epsilon: f32,
scale: f32,
}
impl Default for MandelbulbSettings {
fn default() -> Self {
Self {
power: 8.,
iterations: 256,
max_distance: 2.,
max_steps: 128,
ray_epsilon: 0.001,
normal_epsilon: 0.001,
scale: 0.9,
}
}
}
fn update_volu_material(
q: Query<(&Transform, &MeshMaterial3d<MandelbulbMaterial>)>,
mut volu_materials: ResMut<Assets<MandelbulbMaterial>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
settings: Res<MandelbulbSettings>,
) {
for (transform, material) in q.iter() {
let volu_material = volu_materials.get_mut(material.0.id()).unwrap();
let buffer = buffers
.get_mut(volu_material.extension.storage.id())
.unwrap();
let MandelbulbSettings {
power,
iterations,
max_distance,
max_steps,
ray_epsilon,
normal_epsilon,
scale,
} = *settings;
let volu_storage = MandelbulbStorage {
mesh_translation: transform.translation,
model_inverse: transform.compute_matrix().inverse(),
power,
iterations,
max_distance,
max_steps,
ray_epsilon,
normal_epsilon,
scale,
};
buffer.set_data(volu_storage);
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
pub struct MandelbulbExtension {
#[storage(100, read_only)]
storage: Handle<ShaderStorageBuffer>,
}
impl MaterialExtension for MandelbulbExtension {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
}
impl MandelbulbExtension {
pub fn create_material(
base: StandardMaterial,
storage: Handle<ShaderStorageBuffer>,
) -> MandelbulbMaterial {
MandelbulbMaterial {
base,
extension: Self { storage },
}
}
}
#[derive(ShaderType, Clone, Default, Debug)]
pub struct MandelbulbStorage {
mesh_translation: Vec3,
model_inverse: Mat4,
power: f32,
iterations: i32,
max_distance: f32,
max_steps: i32,
ray_epsilon: f32,
normal_epsilon: f32,
scale: f32,
}