import math
import random
from dataclasses import dataclass, field
import numpy as np
try:
import numba except ImportError:
print("ERROR: Numba is required for this example.")
print("Install with: pip install numba")
exit(1)
from pybevy.camera import Exposure
from pybevy.light import VolumetricFog, VolumetricLight
from pybevy.prelude import *
PARTICLE_COUNT = 20000
CRYSTAL_COUNT = 7
SPOTLIGHT_COUNT = 3
@component
@dataclass
class Particle(Component):
vel: Vec3 = field(default_factory=lambda: Vec3.ZERO)
lifetime: float = 0.0
max_lifetime: float = 10.0
@component
class Crystal(Component):
rotation_speed: float
phase_offset: float
def __init__(self, rotation_speed: float = 1.0, phase_offset: float = 0.0):
self.rotation_speed = rotation_speed
self.phase_offset = phase_offset
@component
@dataclass
class SweepingLight(Component):
sweep_angle: float = 0.0
sweep_speed: float = 0.5
center: Vec3 = field(default_factory=lambda: Vec3.ZERO)
@component
class CinematicCamera(Component):
def setup_cave(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
cave_material = materials.add(
StandardMaterial(
base_color=Color.srgb(0.15, 0.12, 0.1),
perceptual_roughness=0.9,
metallic=0.1,
)
)
floor_mesh = meshes.add(Plane3d().mesh().size(50.0, 50.0).build())
commands.spawn(
Mesh3d(floor_mesh),
MeshMaterial3d(cave_material),
Transform.from_xyz(0.0, -2.0, 0.0),
)
commands.spawn(
Mesh3d(floor_mesh),
MeshMaterial3d(cave_material),
Transform.from_xyz(0.0, 12.0, 0.0),
)
wall_mesh = meshes.add(Cuboid(50.0, 14.0, 1.0))
commands.spawn(
Mesh3d(wall_mesh),
MeshMaterial3d(cave_material),
Transform.from_xyz(0.0, 5.0, -25.0),
)
commands.spawn(
Mesh3d(wall_mesh),
MeshMaterial3d(cave_material),
Transform.from_xyz(0.0, 5.0, 25.0),
)
commands.spawn(
Mesh3d(meshes.add(Cuboid(1.0, 14.0, 50.0))),
MeshMaterial3d(cave_material),
Transform.from_xyz(-25.0, 5.0, 0.0),
)
commands.spawn(
Mesh3d(meshes.add(Cuboid(1.0, 14.0, 50.0))),
MeshMaterial3d(cave_material),
Transform.from_xyz(25.0, 5.0, 0.0),
)
def setup_crystals(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
crystal_mesh = meshes.add(Cylinder(0.6, 3.0))
crystal_colors = [
LinearRgba.rgb(100.0, 20.0, 150.0), LinearRgba.rgb(20.0, 150.0, 200.0), LinearRgba.rgb(200.0, 50.0, 100.0), LinearRgba.rgb(150.0, 200.0, 50.0), LinearRgba.rgb(200.0, 100.0, 20.0), LinearRgba.rgb(50.0, 200.0, 150.0), LinearRgba.rgb(180.0, 50.0, 200.0), ]
radius = 8.0
for i in range(CRYSTAL_COUNT):
angle = (i / CRYSTAL_COUNT) * 2.0 * math.pi
x = math.cos(angle) * radius
z = math.sin(angle) * radius
color = crystal_colors[i % len(crystal_colors)]
crystal_material = materials.add(
StandardMaterial(
emissive=color,
metallic=0.9,
perceptual_roughness=0.1,
)
)
commands.spawn(
Mesh3d(crystal_mesh),
MeshMaterial3d(crystal_material),
Transform.from_xyz(x, 1.5, z).with_rotation(
Quat.from_rotation_z(math.pi / 6.0) ),
Crystal(
rotation_speed=0.3 + random.random() * 0.5,
phase_offset=random.random() * math.pi * 2.0,
),
)
commands.spawn(
PointLight(
intensity=5000.0,
range=15.0,
radius=0.5,
color=Color.linear_rgb(color.red / 200.0, color.green / 200.0, color.blue / 200.0),
shadows_enabled=False,
),
Transform.from_xyz(x, 1.5, z),
)
def setup_volumetric_lights(commands: Commands) -> None:
spotlight_configs = [
(10.0, 10.0, -10.0, 0.0, 0.0, 0.0, 1.0, 0.3, 0.3, 0.3), (-10.0, 10.0, 10.0, 0.0, 0.0, 0.0, 0.3, 1.0, 0.8, 0.5), (0.0, 11.0, 0.0, 0.0, 0.0, 0.0, 0.8, 0.3, 1.0, 0.7), ]
for _i, (x, y, z, tx, ty, tz, r, g, b, speed) in enumerate(spotlight_configs):
commands.spawn(
SpotLight(
intensity=50000.0,
range=30.0,
radius=0.3,
color=Color.srgb(r, g, b),
shadows_enabled=True,
inner_angle=0.3,
outer_angle=0.6,
),
Transform.from_xyz(x, y, z).looking_at(Vec3(tx, ty, tz), Vec3.Y),
VolumetricLight(), SweepingLight(
sweep_speed=speed,
center=Vec3(x, y, z),
),
)
commands.spawn(
DirectionalLight(
illuminance=500.0,
color=Color.srgb(0.6, 0.7, 0.9),
shadows_enabled=False,
),
Transform.IDENTITY.looking_at(Vec3(-0.3, -1.0, -0.5), Vec3.Y),
)
commands.insert_resource(GlobalAmbientLight(brightness=50.0, color=Color.srgb(0.5, 0.6, 0.8)))
def spawn_particles(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
particle_mesh = meshes.add(Sphere(0.02))
particle_material = materials.add(
StandardMaterial(
emissive=LinearRgba.rgb(0.5, 0.5, 0.5),
base_color=Color.srgba(1.0, 1.0, 1.0, 0.8),
alpha_mode=AlphaMode.Blend(),
)
)
for _ in range(PARTICLE_COUNT):
x = random.uniform(-15.0, 15.0)
y = random.uniform(-1.0, 10.0)
z = random.uniform(-15.0, 15.0)
vx = random.uniform(-0.5, 0.5)
vy = random.uniform(-0.2, 0.5)
vz = random.uniform(-0.5, 0.5)
commands.spawn(
Mesh3d(particle_mesh),
MeshMaterial3d(particle_material),
Transform.from_xyz(x, y, z).with_scale(Vec3.splat(0.5 + random.random() * 1.0)),
Particle(
vel=Vec3(vx, vy, vz),
lifetime=random.random() * 5.0,
max_lifetime=8.0 + random.random() * 4.0,
),
)
def setup_post_processing(commands: Commands) -> None:
commands.spawn(
Camera3d(),
Transform.from_xyz(0.0, 3.0, 15.0).looking_at(Vec3(0.0, 2.0, 0.0), Vec3.Y),
Hdr(),
Exposure.INDOOR,
Tonemapping.TONY_MC_MAPFACE,
Bloom(
intensity=0.3,
low_frequency_boost=0.8,
low_frequency_boost_curvature=0.95,
high_pass_frequency=1.0,
composite_mode=BloomCompositeMode.EnergyConserving,
),
ColorGrading(
global_=ColorGradingGlobal(
exposure=0.0,
temperature=-0.2, tint=0.1, post_saturation=1.3, ),
shadows=ColorGradingSection(
saturation=1.5, contrast=1.1,
),
highlights=ColorGradingSection(
saturation=0.9, contrast=1.0,
),
),
VolumetricFog(
ambient_color=Color.srgb(0.3, 0.4, 0.6),
ambient_intensity=0.05,
step_count=64, jitter=0.5,
),
CinematicCamera(),
)
@numba.jit(nopython=True) def particle_physics_kernel(
pos, vel, lifetime: np.ndarray, max_lifetime: np.ndarray, delta_time: float,
) -> None:
n = len(pos.x)
for i in numba.prange(n):
lifetime[i] += delta_time
if lifetime[i] >= max_lifetime[i]:
lifetime[i] = 0.0
pos.x[i] = random.uniform(-15.0, 15.0)
pos.y[i] = random.uniform(-1.0, 10.0)
pos.z[i] = random.uniform(-15.0, 15.0)
vel.x[i] = random.uniform(-0.5, 0.5)
vel.y[i] = random.uniform(-0.2, 0.5)
vel.z[i] = random.uniform(-0.5, 0.5)
vel.x[i] += random.uniform(-0.3, 0.3) * delta_time
vel.y[i] += random.uniform(-0.3, 0.3) * delta_time
vel.z[i] += random.uniform(-0.3, 0.3) * delta_time
vel.x[i] *= 0.99
vel.y[i] *= 0.99
vel.z[i] *= 0.99
vel.y[i] += 0.1 * delta_time
pos.x[i] += vel.x[i] * delta_time
pos.y[i] += vel.y[i] * delta_time
pos.z[i] += vel.z[i] * delta_time
if pos.x[i] < -15.0:
pos.x[i] = 15.0
if pos.x[i] > 15.0:
pos.x[i] = -15.0
if pos.z[i] < -15.0:
pos.z[i] = 15.0
if pos.z[i] > 15.0:
pos.z[i] = -15.0
if pos.y[i] < -1.0:
pos.y[i] = 10.0
if pos.y[i] > 10.0:
pos.y[i] = -1.0
def particle_physics_system(
view: View[tuple[Mut[Transform], Mut[Particle]], With[Particle]],
time: Res[Time],
) -> None:
dt = time.delta_secs()
for batch in view.iter_batches():
pos = batch.column_mut(Transform)
particle_col = batch.column_mut(Particle)
particle_physics_kernel(
pos.translation,
particle_col.vel, particle_col.lifetime, particle_col.max_lifetime, dt,
)
def crystal_rotation_system(
query: Query[tuple[Mut[Transform], Crystal]],
time: Res[Time],
) -> None:
t = time.elapsed_secs()
for transform, crystal in query:
angle = crystal.rotation_speed * t + crystal.phase_offset
transform.rotation = Quat.from_rotation_y(angle) * Quat.from_rotation_z(math.pi / 6.0)
def spotlight_sweep_system(
query: Query[tuple[Mut[Transform], SweepingLight]],
time: Res[Time],
) -> None:
t = time.elapsed_secs()
for transform, light in query:
angle = light.sweep_speed * t
radius = 8.0
target_x = light.center.x + math.cos(angle) * radius
target_z = light.center.z + math.sin(angle) * radius
target_y = 0.0
transform.translation = light.center
target = Vec3(target_x, target_y, target_z)
transform.look_at(target, Vec3.Y)
def camera_cinematic_system(
query: Query[Mut[Transform], With[CinematicCamera]],
time: Res[Time],
) -> None:
t = time.elapsed_secs() * 0.3
for transform in query:
radius = 18.0
height = 5.0 + math.sin(t * 0.5) * 3.0
x = math.cos(t) * radius
z = math.sin(t) * radius
transform.translation = Vec3(x, height, z)
transform.look_at(Vec3(0.0, 2.0, 0.0), Vec3.Y)
@entrypoint
def main(app: App) -> App:
return (
app.add_plugins(DefaultPlugins)
.add_systems(
Startup,
(
setup_cave,
setup_crystals,
setup_volumetric_lights,
spawn_particles,
setup_post_processing,
),
)
.add_systems(
Update,
(
particle_physics_system,
crystal_rotation_system,
spotlight_sweep_system,
camera_cinematic_system,
),
)
)
if __name__ == "__main__":
print(f"Crystalline Cavern: {CRYSTAL_COUNT} crystals, {PARTICLE_COUNT:,} particles, volumetric lighting.")
main().run()