import math
import random
try:
import numba except ImportError:
print("ERROR: Numba is required for this example.")
print("Install with: poetry install --extras numba")
print("Or: pip install numba")
exit(1)
from dataclasses import dataclass
import numpy as np
from pybevy.prelude import *
NUM_PEOPLE = 25000
class CameraPathState:
def __init__(self):
self.waypoints = []
self.current_waypoint = 0
self.initial_wait = 15.0 self.transition_duration = 5.0 self.hold_duration = 2.0 self.transition_start_time = 0.0
self.has_started = False
CAMERA_PATH = CameraPathState()
@component
class Walker(Component):
@component
class FlyingCamera(Component):
@component
class WarningLight(Component):
def __init__(self, phase_offset: float = 0.0):
self.phase_offset = phase_offset
@dataclass
@resource
class CityLayout(Resource):
street_width: float = 2.0
block_size: float = 10.0
num_blocks_x: int = 15
num_blocks_z: int = 15
def setup_city(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
layout = CityLayout()
ground_material = materials.add(Color.srgb(0.3, 0.4, 0.3))
street_material = materials.add(Color.srgb(0.2, 0.2, 0.2))
building_materials = [
materials.add(Color.srgb(0.6, 0.6, 0.7)),
materials.add(Color.srgb(0.7, 0.6, 0.6)),
materials.add(Color.srgb(0.6, 0.7, 0.6)),
materials.add(Color.srgb(0.5, 0.5, 0.6)),
]
city_size = (
layout.num_blocks_x * (layout.block_size + layout.street_width)
) + layout.street_width
ground_plane = Plane3d()
commands.spawn(
Mesh3d(meshes.add(ground_plane.mesh().size(city_size, city_size).build())),
MeshMaterial3d(ground_material),
Transform.from_xyz(city_size / 2.0, -0.05, city_size / 2.0),
)
street_mesh = meshes.add(Cuboid(1.0, 0.1, 1.0))
for x_idx in range(layout.num_blocks_x + 1):
x_pos = x_idx * (layout.block_size + layout.street_width)
street_length = city_size
commands.spawn(
Mesh3d(street_mesh),
MeshMaterial3d(street_material),
Transform(
Vec3(x_pos + layout.street_width / 2.0, 0.0, city_size / 2.0),
Quat.IDENTITY,
Vec3(layout.street_width, 1.0, street_length),
),
)
for z_idx in range(layout.num_blocks_z + 1):
z_pos = z_idx * (layout.block_size + layout.street_width)
street_length = city_size
commands.spawn(
Mesh3d(street_mesh),
MeshMaterial3d(street_material),
Transform(
Vec3(city_size / 2.0, 0.0, z_pos + layout.street_width / 2.0),
Quat.IDENTITY,
Vec3(street_length, 1.0, layout.street_width),
),
)
building_mesh = meshes.add(Cuboid.from_length(1.0))
warning_light_mesh = meshes.add(Sphere(0.2))
warning_light_material = materials.add(Color.srgb(1.0, 0.0, 0.0))
for bx in range(layout.num_blocks_x):
for bz in range(layout.num_blocks_z):
num_buildings = random.randint(1, 4)
block_x = (
bx * (layout.block_size + layout.street_width) + layout.street_width
)
block_z = (
bz * (layout.block_size + layout.street_width) + layout.street_width
)
for _ in range(num_buildings):
width = random.uniform(2.0, 5.0)
depth = random.uniform(2.0, 5.0)
height = random.uniform(5.0, 25.0)
margin = 1.0
x: float = block_x + random.uniform(margin, layout.block_size - width - margin)
z: float = block_z + random.uniform(margin, layout.block_size - depth - margin)
commands.spawn(
Mesh3d(building_mesh),
MeshMaterial3d(random.choice(building_materials)),
Transform(
Vec3(x + width / 2.0, height / 2.0, z + depth / 2.0),
Quat.IDENTITY,
Vec3(width, height, depth),
),
)
if height >= 22.5: building_top = height
center_x = x + width / 2.0
center_z = z + depth / 2.0
corner_offset_x = width / 2.0 * 0.9
corner_offset_z = depth / 2.0 * 0.9
for corner_x in [-corner_offset_x, corner_offset_x]:
for corner_z in [-corner_offset_z, corner_offset_z]:
phase_offset = random.uniform(0.0, 2.0 * math.pi)
commands.spawn(
Mesh3d(warning_light_mesh),
MeshMaterial3d(warning_light_material),
Transform.from_xyz(
center_x + corner_x,
building_top + 0.3,
center_z + corner_z,
),
)
commands.spawn(
PointLight(
intensity=3_000.0,
range=8.0,
radius=0.1,
color=Color.srgb(1.0, 0.0, 0.0),
shadows_enabled=False,
),
Transform.from_xyz(
center_x + corner_x,
building_top + 0.3,
center_z + corner_z,
),
WarningLight(phase_offset),
)
light_spacing = 8.0 light_height = 2.5
light_offset = layout.street_width * 0.6
pole_mesh = meshes.add(Cuboid(0.1, light_height, 0.1))
pole_material = materials.add(Color.srgb(0.3, 0.3, 0.3))
bulb_mesh = meshes.add(Sphere(0.15))
bulb_material = materials.add(Color.srgb(1.0, 0.95, 0.8))
for x in range(layout.num_blocks_x + 1):
x_pos = (
x * (layout.block_size + layout.street_width) + layout.street_width / 2.0
)
num_lights = int(city_size / light_spacing)
for i in range(num_lights):
z_pos = i * light_spacing
for side in [-1, 1]:
light_x = x_pos + (side * light_offset)
commands.spawn(
Mesh3d(pole_mesh),
MeshMaterial3d(pole_material),
Transform.from_xyz(light_x, light_height / 2.0, z_pos),
)
commands.spawn(
Mesh3d(bulb_mesh),
MeshMaterial3d(bulb_material),
Transform.from_xyz(light_x, light_height, z_pos),
)
commands.spawn(
PointLight(
intensity=2_000.0,
range=12.0,
radius=0.2,
color=Color.srgb(1.0, 0.9, 0.7),
shadows_enabled=False,
),
Transform.from_xyz(light_x, light_height, z_pos),
)
for z in range(layout.num_blocks_z + 1):
z_pos = (
z * (layout.block_size + layout.street_width) + layout.street_width / 2.0
)
num_lights = int(city_size / light_spacing)
for i in range(num_lights):
x_pos = i * light_spacing
for side in [-1, 1]:
light_z = z_pos + (side * light_offset)
commands.spawn(
Mesh3d(pole_mesh),
MeshMaterial3d(pole_material),
Transform.from_xyz(x_pos, light_height / 2.0, light_z),
)
commands.spawn(
Mesh3d(bulb_mesh),
MeshMaterial3d(bulb_material),
Transform.from_xyz(x_pos, light_height, light_z),
)
commands.spawn(
PointLight(
intensity=5_000.0,
range=12.0,
radius=0.2,
color=Color.srgb(1.0, 0.9, 0.7),
shadows_enabled=False,
),
Transform.from_xyz(x_pos, light_height, light_z),
)
def spawn_people(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
layout = CityLayout()
city_size = (
layout.num_blocks_x * (layout.block_size + layout.street_width)
) + layout.street_width
person_mesh = meshes.add(Cuboid(0.15, 0.6, 0.16))
person_materials = [
materials.add(Color.srgb(0.8, 0.2, 0.2)),
materials.add(Color.srgb(0.2, 0.8, 0.2)),
materials.add(Color.srgb(0.2, 0.2, 0.8)),
materials.add(Color.srgb(0.8, 0.8, 0.2)),
materials.add(Color.srgb(0.8, 0.2, 0.8)),
materials.add(Color.srgb(0.2, 0.8, 0.8)),
]
for _ in range(NUM_PEOPLE):
if random.random() < 0.5:
street_idx = random.randint(0, layout.num_blocks_x)
x = (
street_idx * (layout.block_size + layout.street_width)
+ layout.street_width / 2.0
)
x += random.uniform(-layout.street_width / 3.0, layout.street_width / 3.0)
z = random.uniform(0, city_size)
else:
street_idx = random.randint(0, layout.num_blocks_z)
z = (
street_idx * (layout.block_size + layout.street_width)
+ layout.street_width / 2.0
)
z += random.uniform(-layout.street_width / 3.0, layout.street_width / 3.0)
x = random.uniform(0, city_size)
angle = random.uniform(0, 2 * math.pi)
person_y = 0.3
commands.spawn(
Mesh3d(person_mesh),
MeshMaterial3d(random.choice(person_materials)),
Transform.from_xyz(x, person_y, z).with_rotation(Quat.from_rotation_y(angle)),
Walker(),
)
def setup_scene(
commands: Commands,
) -> None:
layout = CityLayout()
city_size = (
layout.num_blocks_x * (layout.block_size + layout.street_width)
) + layout.street_width
center = city_size / 2.0
camera_path = CAMERA_PATH
camera_path.waypoints.append(
(
np.array([center, 150.0, city_size + 120.0]), np.array([center, 0.0, center]), )
)
camera_path.waypoints.append(
(
np.array([center, 150.0, city_size + 80.0]), np.array([center, 0.0, center]), )
)
street_x = layout.street_width * 2.5 + layout.block_size * 2
start_z = center - 20.0
camera_path.waypoints.append(
(
np.array([street_x - 3.0, 2.5, start_z]), np.array(
[street_x + 5.0, 2.0, start_z + 25.0]
), )
)
corner_x = layout.street_width + layout.block_size * 1.0
corner_z = layout.street_width + layout.block_size * 1.0
camera_path.waypoints.append(
(
np.array([corner_x - 12.0, 35.0, corner_z - 12.0]), np.array(
[corner_x + 30.0, 8.0, corner_z + 30.0]
), )
)
building_x = layout.street_width + layout.block_size / 2.0
building_z = layout.street_width + layout.block_size / 2.0
camera_path.waypoints.append(
(
np.array([building_x - 15.0, 20.0, building_z + 15.0]),
np.array([building_x, 10.0, building_z]),
)
)
alley_center_x = layout.street_width + layout.block_size * 4.5
alley_center_z = layout.street_width * 5 + layout.block_size * 4.5
camera_path.waypoints.append(
(
np.array([alley_center_x, 12.0, alley_center_z]),
np.array([alley_center_x - 15.0, 0.0, alley_center_z + 10.0]),
)
)
mid_flight_x = center + 20.0
mid_flight_z = center + 20.0
camera_path.waypoints.append(
(
np.array([mid_flight_x, 45.0, mid_flight_z]),
np.array([mid_flight_x + 30.0, 20.0, mid_flight_z + 30.0]), )
)
camera_path.waypoints.append(
(
np.array(
[mid_flight_x + 35.0, 25.0, mid_flight_z + 35.0]
), np.array(
[mid_flight_x - 10.0, 5.0, mid_flight_z - 10.0]
), )
)
camera_path.waypoints.append(
(np.array([center, 150.0, city_size + 120.0]), np.array([center, 0.0, center]))
)
start_pos, start_target = camera_path.waypoints[0]
commands.spawn(
Camera3d(),
Transform.from_xyz(start_pos[0], start_pos[1], start_pos[2]).looking_at(
Vec3(start_target[0], start_target[1], start_target[2]), Vec3.Y
),
FlyingCamera(),
)
@numba.jit(nopython=True) def walking_kernel(
translation,
rotation,
delta_time: float,
city_size: float,
speed: float,
turn_chance: float,
margin: float,
) -> None:
n = len(translation.x)
for i in numba.prange(n): qx = rotation.x[i]
qy = rotation.y[i]
qz = rotation.z[i]
qw = rotation.w[i]
forward_x = -2.0 * (qx * qz + qw * qy)
forward_z = 2.0 * (qx * qx + qy * qy) - 1.0
delta = speed * delta_time
new_x = translation.x[i] + forward_x * delta
new_z = translation.z[i] + forward_z * delta
if random.random() < turn_chance:
turn_angle = random.uniform(-math.pi, math.pi)
half_angle = turn_angle / 2.0
sin_half = math.sin(half_angle)
cos_half = math.cos(half_angle)
new_qx = qw * 0.0 + qx * cos_half + qy * 0.0 - qz * sin_half
new_qy = qw * sin_half - qx * 0.0 + qy * cos_half + qz * 0.0
new_qz = qw * 0.0 + qx * sin_half - qy * 0.0 + qz * cos_half
new_qw = qw * cos_half - qx * 0.0 - qy * sin_half - qz * 0.0
rotation.x[i] = new_qx
rotation.y[i] = new_qy
rotation.z[i] = new_qz
rotation.w[i] = new_qw
bounce = False
if new_x < margin or new_x > city_size - margin:
bounce = True
if new_z < margin or new_z > city_size - margin:
bounce = True
if bounce:
cur_x = rotation.x[i]
cur_y = rotation.y[i]
cur_z = rotation.z[i]
cur_w = rotation.w[i]
rotation.x[i] = -cur_z
rotation.y[i] = cur_w
rotation.z[i] = cur_x
rotation.w[i] = -cur_y
new_x = max(margin, min(city_size - margin, new_x))
new_z = max(margin, min(city_size - margin, new_z))
translation.x[i] = new_x
translation.z[i] = new_z
def walking_system(
view: View[Mut[Transform], With[Walker]],
time: Res[Time],
) -> None:
layout = CityLayout()
city_size = (
layout.num_blocks_x * (layout.block_size + layout.street_width)
) + layout.street_width
speed = 2.0
turn_chance = 0.05
margin = 1.0
for batch in view.iter_batches():
transform = batch.column_mut(Transform)
walking_kernel(
transform.translation,
transform.rotation,
time.delta_secs(),
city_size,
speed,
turn_chance,
margin,
)
def warning_light_pulsate_system(
query: Query[tuple[Mut[PointLight], WarningLight]],
time: Res[Time],
) -> None:
frequency = 0.5
min_intensity = 10_000.0
max_intensity = 50_000.0
base_time = time.elapsed_secs() * frequency * 2.0 * math.pi
for light, warning in query:
t = base_time + warning.phase_offset
pulse = (math.sin(t) + 1.0) / 2.0
light.intensity = min_intensity + (max_intensity - min_intensity) * pulse
@numba.jit(nopython=True) def camera_look_at_kernel(
translation,
rotation,
camera_x: float,
camera_y: float,
camera_z: float,
target_x: float,
target_y: float,
target_z: float,
) -> None:
translation.x[0] = camera_x
translation.y[0] = camera_y
translation.z[0] = camera_z
forward_x = target_x - camera_x
forward_y = target_y - camera_y
forward_z = target_z - camera_z
forward_norm = math.sqrt(
forward_x * forward_x + forward_y * forward_y + forward_z * forward_z
)
if forward_norm > 0.0001:
forward_x /= forward_norm
forward_y /= forward_norm
forward_z /= forward_norm
world_up_x = 0.0
world_up_y = 1.0
world_up_z = 0.0
right_x = forward_y * world_up_z - forward_z * world_up_y
right_y = forward_z * world_up_x - forward_x * world_up_z
right_z = forward_x * world_up_y - forward_y * world_up_x
right_norm = math.sqrt(right_x * right_x + right_y * right_y + right_z * right_z)
if right_norm > 0.0001:
right_x /= right_norm
right_y /= right_norm
right_z /= right_norm
up_x = right_y * forward_z - right_z * forward_y
up_y = right_z * forward_x - right_x * forward_z
up_z = right_x * forward_y - right_y * forward_x
up_norm = math.sqrt(up_x * up_x + up_y * up_y + up_z * up_z)
up_x /= up_norm
up_y /= up_norm
up_z /= up_norm
m00, m01, m02 = right_x, up_x, -forward_x
m10, m11, m12 = right_y, up_y, -forward_y
m20, m21, m22 = right_z, up_z, -forward_z
trace = m00 + m11 + m22
if trace > 0.0:
s = 0.5 / math.sqrt(trace + 1.0)
qw = 0.25 / s
qx = (m21 - m12) * s
qy = (m02 - m20) * s
qz = (m10 - m01) * s
elif m00 > m11 and m00 > m22:
s = 2.0 * math.sqrt(1.0 + m00 - m11 - m22)
qw = (m21 - m12) / s
qx = 0.25 * s
qy = (m01 + m10) / s
qz = (m02 + m20) / s
elif m11 > m22:
s = 2.0 * math.sqrt(1.0 + m11 - m00 - m22)
qw = (m02 - m20) / s
qx = (m01 + m10) / s
qy = 0.25 * s
qz = (m12 + m21) / s
else:
s = 2.0 * math.sqrt(1.0 + m22 - m00 - m11)
qw = (m10 - m01) / s
qx = (m02 + m20) / s
qy = (m12 + m21) / s
qz = 0.25 * s
rotation.x[0] = qx
rotation.y[0] = qy
rotation.z[0] = qz
rotation.w[0] = qw
def camera_flight_system(
view: View[Mut[Transform], With[FlyingCamera]],
time: Res[Time],
) -> None:
camera_path = CAMERA_PATH
current_time = time.elapsed_secs()
if camera_path.transition_start_time == 0.0:
camera_path.transition_start_time = current_time
if not camera_path.has_started:
elapsed_since_start = current_time - camera_path.transition_start_time
if elapsed_since_start < camera_path.initial_wait:
start_pos, start_target = camera_path.waypoints[0]
for batch in view.iter_batches():
transform = batch.column_mut(Transform)
camera_look_at_kernel(
transform.translation,
transform.rotation,
start_pos[0],
start_pos[1],
start_pos[2],
start_target[0],
start_target[1],
start_target[2],
)
return
camera_path.has_started = True
camera_path.transition_start_time = current_time
elapsed = current_time - camera_path.transition_start_time
total_duration = camera_path.transition_duration + camera_path.hold_duration
if elapsed >= total_duration:
camera_path.current_waypoint = (camera_path.current_waypoint + 1) % len(
camera_path.waypoints
)
camera_path.transition_start_time = current_time
elapsed = 0.0
current_idx = camera_path.current_waypoint
next_idx = (current_idx + 1) % len(camera_path.waypoints)
current_pos, current_target = camera_path.waypoints[current_idx]
next_pos, next_target = camera_path.waypoints[next_idx]
if elapsed < camera_path.transition_duration:
t = elapsed / camera_path.transition_duration
t = t * t * (3.0 - 2.0 * t)
camera_pos = current_pos * (1.0 - t) + next_pos * t
look_target = current_target * (1.0 - t) + next_target * t
else:
camera_pos = next_pos
look_target = next_target
for batch in view.iter_batches():
transform = batch.column_mut(Transform)
camera_look_at_kernel(
transform.translation.x,
transform.translation.y,
transform.translation.z,
transform.rotation.x,
transform.rotation.y,
transform.rotation.z,
transform.rotation.w,
camera_pos[0],
camera_pos[1],
camera_pos[2],
look_target[0],
look_target[1],
look_target[2],
)
@entrypoint
def main(app: App) -> App:
return (
app.add_plugins(DefaultPlugins)
.insert_resource(CityLayout())
.add_systems(Startup, (setup_city, spawn_people, setup_scene))
.add_systems(
Update, (walking_system, camera_flight_system, warning_light_pulsate_system)
)
)
if __name__ == "__main__":
print(f"City simulation: {NUM_PEOPLE:,} walking people with View+Numba JIT.")
main().run()