├── assets ├── cells.wgsl ├── compute.wgsl └── shader.wgsl ├── .gitignore ├── Cargo.toml ├── LICENSE └── src ├── rtmaterial.rs ├── main.rs ├── fly_cam.rs ├── ca_compute.rs ├── rule.rs └── gui.rs /assets/cells.wgsl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target 3 | Cargo.lock 4 | */Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cas" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | bevy = "0.9" 10 | bytemuck = { version = "1.13.0", features = ["derive"] } 11 | bevy_egui = "0.19" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Isidor Nielsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/rtmaterial.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU64; 2 | 3 | use bevy::{pbr::MaterialPipeline, prelude::*, reflect::TypeUuid, render::render_resource::*}; 4 | 5 | use crate::rule::{GPURule, Rule}; 6 | 7 | #[derive(Debug, Clone, TypeUuid)] 8 | #[uuid = "1ae9c363-1234-4213-890e-192d81b00281"] 9 | pub struct RTVolumeMaterial { 10 | pub volume: Option>, 11 | pub rule: Rule, 12 | } 13 | 14 | impl AsBindGroup for RTVolumeMaterial { 15 | type Data = (); 16 | 17 | fn as_bind_group( 18 | &self, 19 | layout: &BindGroupLayout, 20 | render_device: &bevy::render::renderer::RenderDevice, 21 | images: &bevy::render::render_asset::RenderAssets, 22 | _fallback_image: &bevy::render::texture::FallbackImage, 23 | ) -> Result, AsBindGroupError> { 24 | let volume = self 25 | .volume 26 | .as_ref() 27 | .ok_or(AsBindGroupError::RetryNextUpdate)?; 28 | let image = images 29 | .get(volume) 30 | .ok_or(AsBindGroupError::RetryNextUpdate)?; 31 | 32 | let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { 33 | label: Some("rule_buffer"), 34 | contents: bytemuck::bytes_of(&GPURule::from(&self.rule)), 35 | usage: BufferUsages::UNIFORM, 36 | }); 37 | 38 | let bind_group = render_device.create_bind_group(&BindGroupDescriptor { 39 | label: None, 40 | layout, 41 | entries: &[ 42 | BindGroupEntry { 43 | binding: 0, 44 | resource: BindingResource::TextureView(&image.texture_view), 45 | }, 46 | BindGroupEntry { 47 | binding: 1, 48 | resource: buffer.as_entire_binding(), 49 | }, 50 | ], 51 | }); 52 | 53 | Ok(PreparedBindGroup { 54 | bindings: vec![OwnedBindingResource::Buffer(buffer)], 55 | bind_group, 56 | data: (), 57 | }) 58 | } 59 | 60 | fn bind_group_layout(render_device: &bevy::render::renderer::RenderDevice) -> BindGroupLayout { 61 | render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { 62 | label: None, 63 | entries: &[ 64 | BindGroupLayoutEntry { 65 | binding: 0, 66 | visibility: ShaderStages::FRAGMENT | ShaderStages::COMPUTE, 67 | ty: BindingType::StorageTexture { 68 | access: StorageTextureAccess::ReadWrite, 69 | format: TextureFormat::R8Uint, 70 | view_dimension: TextureViewDimension::D3, 71 | }, 72 | count: None, 73 | }, 74 | BindGroupLayoutEntry { 75 | binding: 1, 76 | visibility: ShaderStages::FRAGMENT | ShaderStages::COMPUTE, 77 | ty: BindingType::Buffer { 78 | ty: BufferBindingType::Uniform, 79 | has_dynamic_offset: false, 80 | min_binding_size: Some(NonZeroU64::new(64).unwrap()), 81 | }, 82 | count: None, 83 | }, 84 | ], 85 | }) 86 | } 87 | } 88 | 89 | impl Material for RTVolumeMaterial { 90 | fn vertex_shader() -> ShaderRef { 91 | "shader.wgsl".into() 92 | } 93 | 94 | fn fragment_shader() -> ShaderRef { 95 | "shader.wgsl".into() 96 | } 97 | 98 | fn specialize( 99 | _pipeline: &MaterialPipeline, 100 | descriptor: &mut RenderPipelineDescriptor, 101 | _layout: &bevy::render::mesh::MeshVertexBufferLayout, 102 | _key: bevy::pbr::MaterialPipelineKey, 103 | ) -> Result<(), SpecializedMeshPipelineError> { 104 | descriptor.primitive.cull_mode = None; 105 | Ok(()) 106 | } 107 | } 108 | 109 | pub struct RTMatPlugin; 110 | 111 | impl Plugin for RTMatPlugin { 112 | fn build(&self, app: &mut App) { 113 | app.add_plugin(MaterialPlugin::::default()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod ca_compute; 2 | mod fly_cam; 3 | mod gui; 4 | mod rtmaterial; 5 | mod rule; 6 | 7 | use bevy::{diagnostic::FrameTimeDiagnosticsPlugin, prelude::*, render::render_resource::*}; 8 | use ca_compute::{CAImage, CAPlugin}; 9 | use fly_cam::{MovementSettings, PlayerPlugin}; 10 | use gui::GuiPlugin; 11 | use rtmaterial::{RTMatPlugin, RTVolumeMaterial}; 12 | use rule::{Rule, RulePlugin}; 13 | 14 | const WORKGROUP_SIZE: u32 = 9; 15 | 16 | const START_SPEED: f32 = 0.1; 17 | const START_SENSITIVITY: f32 = 0.0004; 18 | 19 | fn main() { 20 | App::new() 21 | .add_startup_system(setup) 22 | .insert_resource(ClearColor(Color::BLACK)) 23 | .add_plugins( 24 | DefaultPlugins 25 | .set(WindowPlugin { 26 | window: WindowDescriptor { 27 | present_mode: bevy::window::PresentMode::Immediate, 28 | title: "Cellular Automata".to_string(), 29 | ..Default::default() 30 | }, 31 | add_primary_window: true, 32 | exit_on_all_closed: true, 33 | close_when_requested: true, 34 | }) 35 | .set(AssetPlugin { 36 | watch_for_changes: true, 37 | ..default() 38 | }), 39 | ) 40 | .add_plugin(FrameTimeDiagnosticsPlugin) 41 | .insert_resource(MovementSettings { 42 | sensitivity: START_SENSITIVITY, 43 | speed: START_SPEED, 44 | }) 45 | .add_plugin(PlayerPlugin) 46 | .add_plugin(RulePlugin) 47 | .add_plugin(CAPlugin) 48 | .add_plugin(RTMatPlugin) 49 | .add_plugin(GuiPlugin) 50 | .add_system(update_size) 51 | .add_system(update_shape) 52 | .run(); 53 | } 54 | 55 | #[derive(Resource)] 56 | struct CurrentSize(u32); 57 | 58 | #[derive(Resource)] 59 | struct Meshes { 60 | meshes: Vec<(&'static str, Handle)>, 61 | current: usize, 62 | } 63 | 64 | fn setup( 65 | mut commands: Commands, 66 | mut images: ResMut>, 67 | mut materials: ResMut>, 68 | mut meshes: ResMut>, 69 | rule: Res, 70 | ) { 71 | let mut image = Image::new_fill( 72 | Extent3d { 73 | width: rule.size, 74 | height: rule.size, 75 | depth_or_array_layers: rule.size, 76 | }, 77 | bevy::render::render_resource::TextureDimension::D3, 78 | &[0], 79 | TextureFormat::R8Uint, 80 | ); 81 | image.texture_descriptor.usage = 82 | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST; 83 | 84 | let meshes = Meshes { 85 | current: 0, 86 | meshes: vec![ 87 | ("Box", meshes.add(Mesh::from(shape::Cube { size: 2.0 }))), 88 | ( 89 | "Sphere", 90 | meshes.add(Mesh::from(shape::UVSphere { 91 | radius: 1.0, 92 | sectors: 50, 93 | stacks: 50, 94 | })), 95 | ), 96 | ( 97 | "Torus", 98 | meshes.add(Mesh::from(shape::Torus { 99 | radius: 0.8, 100 | ring_radius: 0.2, 101 | subdivisions_segments: 75, 102 | subdivisions_sides: 50, 103 | })), 104 | ), 105 | ], 106 | }; 107 | let image = images.add(image); 108 | commands.spawn(MaterialMeshBundle:: { 109 | mesh: meshes.meshes[meshes.current].1.clone(), 110 | material: materials.add(RTVolumeMaterial { 111 | volume: Some(image.clone()), 112 | rule: rule.clone(), 113 | }), 114 | ..default() 115 | }); 116 | 117 | commands.insert_resource(meshes); 118 | commands.insert_resource(CurrentSize(rule.size)); 119 | commands.insert_resource(CAImage(image)); 120 | } 121 | 122 | fn update_size( 123 | image: Res, 124 | mut images: ResMut>, 125 | rule: Res, 126 | mut size: ResMut, 127 | ) { 128 | if size.0 != rule.size { 129 | size.0 = rule.size; 130 | if let Some(image) = images.get_mut(&image.0) { 131 | image.resize(Extent3d { 132 | width: rule.size, 133 | height: rule.size, 134 | depth_or_array_layers: rule.size, 135 | }); 136 | } 137 | } 138 | } 139 | 140 | fn update_shape(meshes: Res, mut mesh: Query<&mut Handle>, mut last: Local) { 141 | if *last != meshes.current { 142 | for mut mesh in mesh.iter_mut() { 143 | *mesh = meshes.meshes[meshes.current].1.clone(); 144 | } 145 | *last = meshes.current; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /assets/compute.wgsl: -------------------------------------------------------------------------------- 1 | 2 | @group(0) @binding(0) 3 | var r_cells: texture_storage_3d; 4 | 5 | fn get_cell(pos: vec3, offset_x: i32, offset_y: i32, offset_z: i32) -> i32 { 6 | let value: vec4 = textureLoad(r_cells, pos + vec3(offset_x, offset_y, offset_z)); 7 | return i32(value.x); 8 | } 9 | 10 | struct Rule { 11 | size: u32, 12 | spawn_mode: u32, 13 | spawn_chance: f32, 14 | survival: u32, 15 | birth: u32, 16 | states: u32, 17 | neighbor_mode: u32, 18 | color_mode: u32, 19 | color0: vec4, 20 | color1: vec4, 21 | }; 22 | 23 | @group(0) @binding(1) 24 | var r_rule: Rule; 25 | 26 | fn is_alive(value: i32) -> i32 { 27 | return value / i32(r_rule.states); 28 | } 29 | 30 | fn should_survive(num_neighbours: i32) -> bool { 31 | return ((r_rule.survival >> u32(num_neighbours)) & u32(1)) != u32(0); 32 | } 33 | 34 | fn should_birth(num_neighbours: i32) -> bool { 35 | return ((r_rule.birth >> u32(num_neighbours)) & u32(1)) != u32(0); 36 | } 37 | 38 | fn count_alive(pos: vec3) -> i32 { 39 | switch i32(r_rule.neighbor_mode) { 40 | case 0: { 41 | return is_alive(get_cell(pos, -1, -1, -1)) + 42 | is_alive(get_cell(pos, -1, -1, 0)) + 43 | is_alive(get_cell(pos, -1, -1, 1)) + 44 | is_alive(get_cell(pos, -1, 0, -1)) + 45 | is_alive(get_cell(pos, -1, 0, 0)) + 46 | is_alive(get_cell(pos, -1, 0, 1)) + 47 | is_alive(get_cell(pos, -1, 1, -1)) + 48 | is_alive(get_cell(pos, -1, 1, 0)) + 49 | is_alive(get_cell(pos, -1, 1, 1)) + 50 | 51 | is_alive(get_cell(pos, 0, -1, -1)) + 52 | is_alive(get_cell(pos, 0, -1, 0)) + 53 | is_alive(get_cell(pos, 0, -1, 1)) + 54 | is_alive(get_cell(pos, 0, 0, -1)) + 55 | //is_alive(get_cell(pos, 0, 0, 0)) + Don't count yourself 56 | is_alive(get_cell(pos, 0, 0, 1)) + 57 | is_alive(get_cell(pos, 0, 1, -1)) + 58 | is_alive(get_cell(pos, 0, 1, 0)) + 59 | is_alive(get_cell(pos, 0, 1, 1)) + 60 | 61 | is_alive(get_cell(pos, 1, -1, -1)) + 62 | is_alive(get_cell(pos, 1, -1, 0)) + 63 | is_alive(get_cell(pos, 1, -1, 1)) + 64 | is_alive(get_cell(pos, 1, 0, -1)) + 65 | is_alive(get_cell(pos, 1, 0, 0)) + 66 | is_alive(get_cell(pos, 1, 0, 1)) + 67 | is_alive(get_cell(pos, 1, 1, -1)) + 68 | is_alive(get_cell(pos, 1, 1, 0)) + 69 | is_alive(get_cell(pos, 1, 1, 1)); 70 | } 71 | case 1: { 72 | return is_alive(get_cell(pos, 0, 0, -1)) + 73 | is_alive(get_cell(pos, 0, 0, 1)) + 74 | is_alive(get_cell(pos, 0, -1, 0)) + 75 | is_alive(get_cell(pos, 0, 1, 0)) + 76 | is_alive(get_cell(pos, -1, 0, 0)) + 77 | is_alive(get_cell(pos, 1, 0, 0)); 78 | } 79 | default: { 80 | return 0; 81 | } 82 | } 83 | } 84 | 85 | fn hash(value: u32) -> u32 { 86 | var state = value; 87 | state = state ^ 2747636419u; 88 | state = state * 2654435769u; 89 | state = state ^ state >> 16u; 90 | state = state * 2654435769u; 91 | state = state ^ state >> 16u; 92 | state = state * 2654435769u; 93 | return state; 94 | } 95 | 96 | fn random_float(value: u32) -> f32 { 97 | return f32(hash(value)) / 4294967295.0; 98 | } 99 | 100 | @compute @workgroup_size(9, 9, 9) 101 | fn init(@builtin(global_invocation_id) pos: vec3) { 102 | var alive = false; 103 | switch i32(r_rule.spawn_mode) { 104 | // Random 105 | case 0: { 106 | let random_number = random_float(pos.z * r_rule.size * r_rule.size + pos.y * r_rule.size + pos.x); 107 | alive = random_number > r_rule.spawn_chance; 108 | } 109 | // Menger Sponge 110 | case 1: { 111 | let size = r_rule.size; 112 | var i = u32(3); 113 | loop { 114 | let s = size / i; 115 | if size - s * i != u32(0) { 116 | alive = true; 117 | break; 118 | } 119 | let p = abs(vec3((pos / s) - (pos / s) / u32(3) * u32(3)) - vec3(1)); 120 | if p.x + p.y + p.z <= 1 { 121 | break; 122 | } 123 | i = i * u32(3); 124 | } 125 | } 126 | default: {} 127 | } 128 | 129 | 130 | textureStore(r_cells, vec3(pos), vec4(u32(alive) * u32(r_rule.states))); 131 | } 132 | 133 | @compute @workgroup_size(9, 9, 9) 134 | fn update(@builtin(global_invocation_id) invocation_id: vec3) { 135 | let pos = vec3(invocation_id); 136 | var cur = get_cell(pos, 0, 0, 0); 137 | 138 | let alive = count_alive(pos); 139 | 140 | if is_alive(cur) == 1 { 141 | if !should_survive(alive) { 142 | cur = cur - 1; 143 | } 144 | } else if cur == 0 { 145 | if should_birth(alive) { 146 | cur = i32(r_rule.states); 147 | } 148 | } else { 149 | cur = cur - 1; 150 | } 151 | 152 | let alive = count_alive(pos); 153 | 154 | let res = u32(cur); 155 | storageBarrier(); 156 | textureStore(r_cells, pos, vec4(res)); 157 | } -------------------------------------------------------------------------------- /src/fly_cam.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::event::Events, prelude::*, window::CursorGrabMode}; 2 | use bevy::{ecs::event::ManualEventReader, input::mouse::MouseMotion}; 3 | 4 | /// Keeps track of mouse motion events, pitch, and yaw 5 | #[derive(Default, Resource)] 6 | struct InputState { 7 | reader_motion: ManualEventReader, 8 | pitch: f32, 9 | yaw: f32, 10 | } 11 | 12 | #[derive(Resource)] 13 | /// Mouse sensitivity and movement speed 14 | pub struct MovementSettings { 15 | pub sensitivity: f32, 16 | pub speed: f32, 17 | } 18 | 19 | impl Default for MovementSettings { 20 | fn default() -> Self { 21 | Self { 22 | sensitivity: 0.00012, 23 | speed: 12., 24 | } 25 | } 26 | } 27 | 28 | /// Used in queries when you want flycams and not other cameras 29 | #[derive(Component)] 30 | pub struct FlyCam; 31 | 32 | /// Grabs/ungrabs mouse cursor 33 | fn toggle_grab_cursor(window: &mut Window) { 34 | window.set_cursor_grab_mode(match window.cursor_grab_mode() { 35 | CursorGrabMode::None => CursorGrabMode::Confined, 36 | CursorGrabMode::Confined | CursorGrabMode::Locked => CursorGrabMode::None, 37 | }); 38 | window.set_cursor_visibility(!window.cursor_visible()); 39 | } 40 | 41 | /// Grabs the cursor when game first starts 42 | fn initial_grab_cursor(mut windows: ResMut) { 43 | toggle_grab_cursor(windows.get_primary_mut().unwrap()); 44 | } 45 | 46 | /// Spawns the `Camera3dBundle` to be controlled 47 | fn setup_player(mut commands: Commands) { 48 | let transform = Transform::from_xyz(-2.0, 3.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y); 49 | let rotation = transform.rotation.to_euler(EulerRot::XYZ); 50 | commands.insert_resource(InputState { 51 | pitch: rotation.0, 52 | yaw: rotation.2, 53 | ..Default::default() 54 | }); 55 | commands 56 | .spawn(Camera3dBundle { 57 | transform, 58 | projection: Projection::Perspective(PerspectiveProjection { 59 | near: 0.0001, 60 | far: 15.0, 61 | ..Default::default() 62 | }), 63 | ..Default::default() 64 | }) 65 | .insert(FlyCam); 66 | } 67 | 68 | /// Handles keyboard input and movement 69 | fn player_move( 70 | keys: Res>, 71 | time: Res