import math
from typing import TYPE_CHECKING
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 pybevy.contrib import OrbitCamera, OrbitCameraPlugin
from pybevy.prelude import *
if TYPE_CHECKING:
from pybevy.ecs import FieldExpr
GRID_SIZE = 387 CUBE_SPACING = 2.0
WAVE_SPEED = 2.0
WAVE_AMPLITUDE = 3.0
@numba.jit(nopython=True, parallel=True)
def animate_wave_kernel(
pos_x: "FieldExpr", pos_y: "FieldExpr", pos_z: "FieldExpr", time: float,
wave_speed: float,
amplitude: float,
) -> None:
n = len(pos_x)
for i in numba.prange(n): x = pos_x[i]
z = pos_z[i]
dist = math.sqrt(x * x + z * z)
wave = math.sin(dist * 0.5 - time * wave_speed) * amplitude
pos_y[i] = wave
@numba.jit(nopython=True, parallel=True)
def animate_spiral_kernel(
pos_x: "FieldExpr",
pos_y: "FieldExpr",
pos_z: "FieldExpr",
rot_y: "FieldExpr", rot_w: "FieldExpr", time: float,
) -> None:
n = len(pos_x)
for i in numba.prange(n): x = pos_x[i]
z = pos_z[i]
dist = math.sqrt(x * x + z * z)
angle = math.atan2(z, x)
wave = math.sin(dist * 0.3 - time * 2.0 + angle) * 2.0
pos_y[i] = wave
rotation_angle = wave * 0.5
half_angle = rotation_angle * 0.5
rot_y[i] = math.sin(half_angle)
rot_w[i] = math.cos(half_angle)
@numba.jit(nopython=True, parallel=True)
def animate_ripple_kernel(
pos_x: "FieldExpr",
pos_y: "FieldExpr",
pos_z: "FieldExpr",
time: float,
) -> None:
n = len(pos_x)
sources = [
(0.0, 0.0),
(50.0, 50.0),
(-50.0, 50.0),
(50.0, -50.0),
(-50.0, -50.0),
]
for i in numba.prange(n): x = pos_x[i]
z = pos_z[i]
height = 0.0
for sx, sz in sources:
dx = x - sx
dz = z - sz
dist = math.sqrt(dx * dx + dz * dz)
ripple = math.sin(dist * 0.3 - time * 3.0) * 2.0 / (1.0 + dist * 0.05)
height += ripple
pos_y[i] = height
@component
class Cube(Component):
def setup_scene(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
cube_material = materials.add(Color.srgb(0.3, 0.7, 0.9))
cube_mesh = meshes.add(Cuboid(1.0, 1.0, 1.0))
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
x = (col - GRID_SIZE / 2) * CUBE_SPACING
z = (row - GRID_SIZE / 2) * CUBE_SPACING
commands.spawn(
Cube(),
Mesh3d(cube_mesh),
MeshMaterial3d(cube_material),
Transform.from_xyz(x, 0.0, z),
)
commands.spawn(
DirectionalLight(
illuminance=10000.0,
color=Color.WHITE,
shadows_enabled=False, ),
Transform.IDENTITY.looking_at(Vec3(-1.0, -2.5, -1.0), Vec3.Y),
)
commands.insert_resource(GlobalAmbientLight(brightness=300.0, color=Color.WHITE))
camera_distance = GRID_SIZE * CUBE_SPACING * 0.8
camera_height = camera_distance * 0.6
target = Vec3(0.0, 0.0, 0.0)
initial_pitch = math.atan2(camera_height, camera_distance)
commands.spawn(
Camera3d(),
Transform.from_xyz(0.0, camera_height, camera_distance).looking_at(
target, Vec3.Y
),
OrbitCamera(
distance=math.sqrt(camera_height**2 + camera_distance**2),
yaw=0.0,
pitch=initial_pitch,
target=target,
),
)
def cube_animation_system(
view: View[Mut[Transform], With[Cube]],
time: Res[Time],
) -> None:
t = time.elapsed_secs()
pattern = int(t / 10.0) % 3
for batch in view.iter_batches():
transform = batch.column_mut(Transform)
if pattern == 0:
animate_wave_kernel(
transform.translation.x,
transform.translation.y,
transform.translation.z,
t,
WAVE_SPEED,
WAVE_AMPLITUDE,
)
elif pattern == 1:
animate_spiral_kernel(
transform.translation.x,
transform.translation.y,
transform.translation.z,
transform.rotation.y,
transform.rotation.w,
t,
)
else:
animate_ripple_kernel(
transform.translation.x,
transform.translation.y,
transform.translation.z,
t,
)
class FPSCounter:
def __init__(self) -> None:
self.frame_count: int = 0
def fps_system(time: Res[Time], counter: Local[FPSCounter]) -> None:
counter.frame_count += 1
t = time.elapsed_secs()
if t > 0 and counter.frame_count % 60 == 0:
print(f"FPS: ~{counter.frame_count / max(t, 0.001):.1f}")
@entrypoint
def main(app: App) -> App:
return (
app.add_plugins(DefaultPlugins)
.add_plugins(OrbitCameraPlugin()) .add_systems(Startup, setup_scene)
.add_systems(Update, (cube_animation_system, fps_system))
)
if __name__ == "__main__":
print(f"View+Numba parallel cube wave: {GRID_SIZE * GRID_SIZE:,} cubes. Drag mouse to rotate camera.")
main().run()