feat: redone visualization to be human-generated

This commit is contained in:
Rendo 2026-08-01 23:44:56 +05:00
commit 5b4ad9a47a
9 changed files with 214 additions and 24 deletions

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.19.0"
bevy = { version = "0.19.0", features = ["bevy_feathers"] }

15
LICENSE Normal file
View file

@ -0,0 +1,15 @@
GRES
Copyright (C) 2026 Veselov Fedor
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -10,7 +10,9 @@ impl<'a> GridManipulationTools<'a> {
Self { grid }
}
pub fn set_xyz(&mut self,x: u32, y: u32, z: u32, element: GridElement) {
let index = self.grid.indexify(UVec3::new(x,y,z));
let Some(index) = self.grid.indexify(UVec3::new(x,y,z)) else {
return;
};
self.grid.data[index] = element;
}
pub fn fill_xyz(&mut self,x1: u32, y1: u32, z1: u32, x2: u32, y2: u32, z2: u32, element: GridElement) {
@ -21,7 +23,9 @@ impl<'a> GridManipulationTools<'a> {
for x in x1..=x2 {
for y in y1..=y2 {
for z in z1..=z2 {
let index = self.grid.indexify(UVec3::new(x,y,z));
let Some(index) = self.grid.indexify(UVec3::new(x,y,z)) else {
continue;
};
self.grid.data[index] = element.clone();
}

View file

@ -26,21 +26,26 @@ impl Grid {
data
}
}
pub fn indexify(&self, vector: UVec3) -> usize{
let clamped_vector = vector.min(self.size - UVec3::ONE);
return (clamped_vector.x + self.size.x * clamped_vector.y + self.size.x * self.size.y * clamped_vector.z) as usize;
pub fn cell_amount(&self) -> u32 {
return self.size.x * self.size.y * self.size.z;
}
pub fn deindexify(&self, index: usize) -> UVec3 {
let undex = index as u32;
pub fn indexify(&self, vector: UVec3) -> Option<usize>{
if vector.x >= self.size.x || vector.y >= self.size.y || vector.z >= self.size.z {
return None;
}
return Some((vector.x + self.size.x * vector.y + self.size.x * self.size.y * vector.z) as usize);
}
pub fn deindexify(&self, index: u32) -> UVec3 {
return UVec3::new(
undex % self.size.x,
undex / self.size.x % self.size.y,
undex / (self.size.x*self.size.y)
index % self.size.x,
index / self.size.x % self.size.y,
index / (self.size.x*self.size.y)
);
}
pub fn tranfer_fluid(&mut self,from: usize, to: usize, transfer_amount: u32) {
let bounds = self.indexify(self.size);
let bounds = self.cell_amount() as usize;
if from >= bounds || to >= bounds {
error!("Trying to transfer fluid out of bounds!");
return;
}

View file

@ -2,6 +2,8 @@ use bevy::prelude::*;
use crate::grid::Grid;
use crate::simulation::tick;
use crate::view::ViewPlugin;
pub mod simulation;
pub mod view;
pub mod grid;
@ -9,8 +11,9 @@ pub mod grid;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(ViewPlugin)
.insert_resource(Grid::cube(16))
.insert_resource(Time::<Fixed>::from_hz(120.))
.insert_resource(Time::<Fixed>::from_hz(20.))
.add_systems(FixedUpdate, tick)
.run();
}

View file

@ -16,7 +16,9 @@ pub fn tick(mut grid: ResMut<Grid>) {
for y in 0..grid.size.y {
for z in 0..grid.size.z {
let vector = UVec3::new(x,y,z);
let index = grid.indexify(vector);
let Some(index) = grid.indexify(vector) else {
continue;
};
match grid.data[index as usize] {
GridElement::Water { amount } => {
@ -24,7 +26,7 @@ pub fn tick(mut grid: ResMut<Grid>) {
if let Some(gravity_vector) = vector.checked_sub(UVec3::new(0,1,0)) {
if let Some(gravity_amount) = grid.data
[grid.indexify(gravity_vector)]
[grid.indexify(gravity_vector).unwrap()]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: gravity_vector, amount: gravity_amount });
transfer_amount -= gravity_amount;
@ -35,18 +37,22 @@ pub fn tick(mut grid: ResMut<Grid>) {
if transfer_amount > 0 {
for lookup_vector in [UVec3::new(1,0,0),UVec3::new(0,0,1)] {
if let Some(toward_vector) = vector.checked_add(lookup_vector) {
if let Some(toward_amount) = grid.data
[grid.indexify(toward_vector)]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
if let Some(index) = grid.indexify(toward_vector) {
if let Some(toward_amount) = grid.data
[index]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
}
}
}
if let Some(toward_vector) = vector.checked_sub(lookup_vector) {
if let Some(toward_amount) = grid.data
[grid.indexify(toward_vector)]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
if let Some(index) = grid.indexify(toward_vector) {
if let Some(toward_amount) = grid.data
[index]
.try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) {
stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount });
}
}
}
}
@ -62,10 +68,11 @@ pub fn tick(mut grid: ResMut<Grid>) {
for action in stack {
match action {
GridAction::MoveLiquid { from, to, amount } => {
let (from_index, to_index) = (grid.indexify(from), grid.indexify(to));
let (from_index, to_index) = (grid.indexify(from).unwrap(), grid.indexify(to).unwrap());
grid.tranfer_fluid(from_index, to_index, amount);
},
}
}
}

142
src/view/game.rs Normal file
View file

@ -0,0 +1,142 @@
use bevy::prelude::*;
use crate::grid::Grid;
use crate::MAX_FLUID_AMOUNT;
const FLUID_FULLNES_STAGES: usize = 7;
pub struct GameViewPlugin;
impl Plugin for GameViewPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreStartup, debug_setup_grid)
.init_resource::<CubeStages>()
.init_resource::<TileMaterials>()
.add_systems(Startup, (setup_view_cubes, scene.spawn()))
.add_systems(Update, update_cubes);
}
}
#[derive(Resource, Default)]
pub struct CubeStages(pub Vec<Handle<Mesh>>);
#[derive(Component, FromTemplate)]
pub struct TileWatch(pub usize);
#[derive(Resource, Default)]
pub struct TileMaterials {
pub solid: Handle<StandardMaterial>,
pub water: Handle<StandardMaterial>,
}
pub fn debug_setup_grid(mut grid: ResMut<Grid>) {
use crate::grid::GridElement;
let mut tool = grid.manipulate();
tool.fill_xyz(5, 0, 0, 5, 0, 14, GridElement::Solid);
tool.fill_xyz(0, 0, 0, 4, 0, 15, GridElement::Water { amount: MAX_FLUID_AMOUNT });
}
pub fn setup_view_cubes(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut stages: ResMut<CubeStages>,
mut tile_materials: ResMut<TileMaterials>,
grid: Res<Grid>) {
for i in 0..FLUID_FULLNES_STAGES {
let tile_height = ((i+1) as f32)/(FLUID_FULLNES_STAGES as f32);
let mesh = meshes.add(Cuboid::new(1., tile_height, 1.));
stages.0.push(mesh);
}
tile_materials.water = materials.add(StandardMaterial {
base_color: Color::hsl(172.,0.502,0.571),
..default()
});
tile_materials.solid = materials.add(StandardMaterial {
base_color: Color::hsl(268., 0.778, 0.499),
..default()
});
let mesh = stages.0[FLUID_FULLNES_STAGES-1].clone();
for i in 0..grid.cell_amount() {
commands.spawn_scene(
cube(
grid.deindexify(i as u32).as_vec3(),
i as usize,
mesh.clone()
)
);
}
}
pub fn scene() -> impl SceneList {
bsn_list![
camera(),
sun()
]
}
pub fn camera() -> impl Scene {
bsn! {
Camera3d
template(
|ctx| {
let grid = ctx.resource::<Grid>();
let center = grid.size.as_vec3() * 0.5;
Ok(Transform::from_xyz(
center.x + grid.size.x as f32 * 1.5,
grid.size.y as f32 * 1.5,
center.z + grid.size.z as f32 * 1.5).looking_at(center, Vec3::Y))
}
)
}
}
pub fn sun() -> impl Scene {
bsn!{
DirectionalLight {
illuminance: 8_000.0,
shadow_maps_enabled: false,
}
template_value(Transform::from_xyz(10.0, 30.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y))
}
}
pub fn cube(position: Vec3,index: usize,mesh: Handle<Mesh>) -> impl Scene {
bsn! {
Mesh3d(mesh)
MeshMaterial3d<StandardMaterial>
Transform::from_translation(position)
TileWatch(index)
Visibility::Hidden
}
}
pub fn update_cubes(query: Query<(&mut Mesh3d,&mut Visibility,&mut MeshMaterial3d<StandardMaterial>,&TileWatch)>, materials: Res<TileMaterials>, stages: Res<CubeStages>, grid: Res<Grid>) {
if grid.is_changed() == false { return; }
for (mut mesh,mut visibility, mut material, index) in query {
let state = grid.data[index.0];
match state {
crate::grid::GridElement::Air => {
*visibility = Visibility::Hidden;
},
crate::grid::GridElement::Water { amount } => {
*visibility = Visibility::Visible;
material.0 = materials.water.clone();
let stage = ((amount as f32)/(MAX_FLUID_AMOUNT as f32) * (FLUID_FULLNES_STAGES-1) as f32).round() as usize;
mesh.0 = stages.0[stage].clone();
},
crate::grid::GridElement::Solid => {
*visibility = Visibility::Visible;
material.0 = materials.solid.clone();
mesh.0 = stages.0[FLUID_FULLNES_STAGES-1].clone();
},
}
}
}

View file

@ -0,0 +1,14 @@
use bevy::prelude::*;
use game::GameViewPlugin;
pub mod ui;
pub mod game;
pub struct ViewPlugin;
impl Plugin for ViewPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(GameViewPlugin);
}
}

0
src/view/ui.rs Normal file
View file