hieroxel / src /render /mod.rs
EHS-Studio's picture
Claude Opus 5.5
Hieroxel: a living voxel world in Rust and Bevy
0e0990f
Raw History Blame Contribute Delete
7.98 kB
//! Chunk material, block textures, and the day/night sky.
pub mod textures;
use std::f32::consts::TAU;
use bevy::asset::embedded_asset;
use bevy::mesh::MeshVertexBufferLayoutRef;
use bevy::pbr::{Material, MaterialPipeline, MaterialPipelineKey, MaterialPlugin};
use bevy::prelude::*;
use bevy::render::render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
};
use bevy::shader::ShaderRef;
use crate::world::block::{BLOCKS, BLOCK_COUNT};
use crate::world::{VoxelWorld, ATTRIBUTE_VOXEL};
const SHADER: &str = "embedded://hieroxel/render/voxel.wesl";
pub struct VoxelRenderPlugin;
impl Plugin for VoxelRenderPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "voxel.wesl");
app.add_plugins(MaterialPlugin::<VoxelMaterial>::default())
.init_resource::<DayCycle>()
.init_resource::<CameraEnv>()
.add_systems(Startup, (setup_materials, setup_sky_bodies))
.add_systems(Update, update_sky);
}
}
#[derive(ShaderType, Clone, Copy, Default, Debug)]
pub struct VoxelParams {
pub fog_color: Vec4,
/// x: daylight, y: unused, z: fog start, w: fog end
pub sky: Vec4,
/// x: underwater, y: water pass
pub misc: Vec4,
}
#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
#[bind_group_data(VoxelMaterialKey)]
pub struct VoxelMaterial {
#[uniform(0)]
pub params: VoxelParams,
#[texture(1, dimension = "2d_array")]
#[sampler(2)]
pub textures: Handle<Image>,
pub water: bool,
}
#[repr(C)]
#[derive(Copy, Clone, Hash, Eq, PartialEq)]
pub struct VoxelMaterialKey {
water: bool,
}
impl From<&VoxelMaterial> for VoxelMaterialKey {
fn from(m: &VoxelMaterial) -> Self {
Self { water: m.water }
}
}
impl Material for VoxelMaterial {
fn vertex_shader() -> ShaderRef {
SHADER.into()
}
fn fragment_shader() -> ShaderRef {
SHADER.into()
}
fn alpha_mode(&self) -> AlphaMode {
if self.water { AlphaMode::Blend } else { AlphaMode::Opaque }
}
fn enable_prepass() -> bool {
false
}
fn enable_shadows() -> bool {
false
}
fn specialize(
_pipeline: &MaterialPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.vertex.buffers = vec![layout.0.get_layout(&[ATTRIBUTE_VOXEL.at_shader_location(0)])?];
if key.bind_group_data.water {
// The surface must be visible from below too.
descriptor.primitive.cull_mode = None;
}
Ok(())
}
}
#[derive(Resource)]
pub struct VoxelMaterials {
pub solid: Handle<VoxelMaterial>,
pub water: Handle<VoxelMaterial>,
/// Hotbar icon per block id (side texture).
pub icons: Vec<Handle<Image>>,
}
#[derive(Resource)]
pub struct DayCycle {
/// 0 = midnight, 0.25 = sunrise, 0.5 = noon, 0.75 = sunset.
pub time: f32,
pub day_length: f32,
pub speed: f32,
}
impl Default for DayCycle {
fn default() -> Self {
Self {
time: 0.3,
day_length: 720.0,
speed: 1.0,
}
}
}
impl DayCycle {
pub fn sun_elevation(&self) -> f32 {
(TAU * (self.time - 0.25)).sin()
}
pub fn clock(&self) -> String {
let minutes = (self.time * 24.0 * 60.0) as u32;
format!("{:02}:{:02}", minutes / 60, minutes % 60)
}
}
/// What the camera is inside; set by the player.
#[derive(Resource, Default)]
pub struct CameraEnv {
pub underwater: bool,
}
fn setup_materials(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<VoxelMaterial>>,
) {
let textures = images.add(textures::build_array());
let solid = materials.add(VoxelMaterial {
params: VoxelParams::default(),
textures: textures.clone(),
water: false,
});
let water = materials.add(VoxelMaterial {
params: VoxelParams {
misc: Vec4::new(0.0, 1.0, 0.0, 0.0),
..default()
},
textures,
water: true,
});
let icons = (0..BLOCK_COUNT)
.map(|id| images.add(textures::icon(BLOCKS[id].tex[1])))
.collect();
commands.insert_resource(VoxelMaterials { solid, water, icons });
}
#[derive(Component)]
struct SkyBody {
/// Offset of the body's angle from the sun's.
phase: f32,
}
fn setup_sky_bodies(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let quad = meshes.add(Rectangle::new(60.0, 60.0));
for (phase, color) in [(0.0, Color::srgb(1.0, 0.93, 0.7)), (0.5, Color::srgb(0.82, 0.86, 0.95))] {
commands.spawn((
SkyBody { phase },
Mesh3d(quad.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: color,
unlit: true,
fog_enabled: false,
cull_mode: None,
..default()
})),
Transform::default(),
));
}
}
fn lerp3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
a + (b - a) * t.clamp(0.0, 1.0)
}
#[allow(clippy::too_many_arguments)]
fn update_sky(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
mut cycle: ResMut<DayCycle>,
env: Res<CameraEnv>,
world: Res<VoxelWorld>,
handles: Option<Res<VoxelMaterials>>,
mut materials: ResMut<Assets<VoxelMaterial>>,
mut clear: ResMut<ClearColor>,
camera: Query<&Transform, (With<Camera3d>, Without<SkyBody>)>,
mut bodies: Query<(&SkyBody, &mut Transform)>,
) {
let fast = if keys.pressed(KeyCode::KeyT) { 60.0 } else { 1.0 };
cycle.time = (cycle.time + time.delta_secs() * cycle.speed * fast / cycle.day_length).rem_euclid(1.0);
let e = cycle.sun_elevation();
let daylight = (e * 2.2 + 0.4).clamp(0.12, 1.0);
let night = Vec3::new(0.015, 0.02, 0.055);
let day = Vec3::new(0.52, 0.74, 1.0);
let dusk = Vec3::new(0.98, 0.56, 0.32);
let mut sky = lerp3(night, day, (e + 0.15) / 0.45);
let glow = (1.0 - e.abs() / 0.3).clamp(0.0, 1.0) * 0.55;
sky = lerp3(sky, dusk, glow);
let rd = world.render_distance as f32 * 32.0;
let (fog_color, fog_start, fog_end) = if env.underwater {
(Vec3::new(0.06, 0.2, 0.42) * daylight.max(0.3), 0.0, 28.0)
} else {
(sky, rd * 0.55, rd * 0.95)
};
let fog_srgb = Color::srgb(fog_color.x, fog_color.y, fog_color.z);
clear.0 = fog_srgb;
let linear = fog_srgb.to_linear();
// Touching a material re-prepares every chunk that uses it, so only write
// when the sky has visibly changed.
if let Some(handles) = handles {
for (handle, water_pass) in [(&handles.solid, 0.0), (&handles.water, 1.0)] {
let params = VoxelParams {
fog_color: Vec4::new(linear.red, linear.green, linear.blue, 1.0),
sky: Vec4::new(daylight, 0.0, fog_start, fog_end),
misc: Vec4::new(env.underwater as u32 as f32, water_pass, 0.0, 0.0),
};
let stale = materials.get(handle).is_some_and(|m| {
let old = m.params;
old.misc != params.misc
|| (old.sky - params.sky).abs().max_element() > 0.004
|| (old.fog_color - params.fog_color).abs().max_element() > 0.003
});
if stale && let Some(mut m) = materials.get_mut(handle) {
m.params = params;
}
}
}
if let Ok(cam) = camera.single() {
for (body, mut t) in &mut bodies {
let angle = TAU * (cycle.time - 0.25 + body.phase);
let dir = Vec3::new(angle.cos(), angle.sin(), 0.25).normalize();
t.translation = cam.translation + dir * 700.0;
t.look_at(cam.translation, Vec3::Y);
}
}
}