Animations

You’ve seen @scene.on_frame used in a few examples already. Now it’s time to understand exactly how it works, what dt means, and how to use it to build smooth, expressive animations.

You’ll also learn about decorators — the Python feature behind the @ symbol.

Open In Jupyter K-12

How on_frame Works

A 3D scene redraws itself many times per second — each redraw is called a frame. @scene.on_frame lets you attach a function that Python calls automatically on every single frame.

The function you write receives one argument: dt — the number of seconds since the last frame. At a typical 60 frames per second, dt is about 0.016.

Run the cell below. The sphere glides back and forth using a sine wave.

Hint: Hold the left mouse button to rotate the camera. Click Stop (■) when you’re done.

import scene3d
import math

scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=12, width=8)

sphere = scene3d.Shapes.Sphere(diameter=1, segments=16)
sphere.set_color('#e94560')
sphere.set_position(0, 1, 0)
scene.add(sphere)

ctx = scene.get_context('2d')
t = 0.0

@scene.on_frame
def animate(dt):
  global t
  t += dt
  x = math.sin(t * 2) * 4
  sphere.set_position(x, 1, 0)

  ctx.clear()
  ctx.fill_style = '#ffffff'
  ctx.font = '20px sans-serif'
  ctx.fill_text(f't = {t:.2f}s', 10, 28)

scene.run()

What Happens Each Frame

Step What happens
1 The scene renders the current frame
2 Python calls your animate(dt) function
3 Your function updates positions, colors, etc.
4 The scene renders the next frame — go to step 1

This loop runs continuously until you click Stop.

In the example above, t grows by dt every frame — a running total of elapsed time. Passing t into math.sin() produces a smooth oscillation between -1 and 1, and multiplying by 4 stretches it to the range -4 to 4.

What dt Is and Why It Matters

dt stands for delta time — the time in seconds since the last frame.

x += speed * dt   # move 'speed' units per second, regardless of frame rate

Multiplying by dt makes your animation frame-rate independent: - On a fast computer running at 120 fps, dt ≈ 0.008 - On a slow computer running at 30 fps, dt ≈ 0.033

The object moves the same distance per second on both machines — just more or fewer steps.

Never write x += 0.05 — that ties your animation speed to the frame rate. Always write x += speed * dt so it’s consistent everywhere.

{ “question_type”: “multiple_choice”, “question”: “What does ‘dt’ represent in the on_frame callback?”, “options”: [ { “key”: “a”, “text”: “The total time since scene.run() was called” }, { “key”: “b”, “text”: “The number of frames rendered so far” }, { “key”: “c”, “text”: “The time in seconds since the last frame” }, { “key”: “d”, “text”: “The desired frame rate” } ], “answer”: “c”, “submitted_answer”: “” }

{ “question_type”: “true_false”, “question”: “Writing ‘x += speed * dt’ makes the animation run at the same apparent speed on fast and slow computers.”, “answer”: “True”, “submitted_answer”: “” }

What Is a Decorator?

The @ in @scene.on_frame is Python’s decorator syntax. A decorator is a shorthand for passing a function to another function automatically.

These two blocks do exactly the same thing:

# Using the decorator — the short way
@scene.on_frame
def animate(dt):
    ...

# Without the decorator — the long way
def animate(dt):
    ...
scene.on_frame(animate)   # manually pass the function to scene.on_frame

When Python sees @scene.on_frame above a function definition, it automatically calls scene.on_frame(animate) right after defining the function. The @ is just a convenience — it saves you from typing the extra line.

Why Decorators Exist

Decorators are a general Python feature used to attach behavior to a function. You’ll see them in many libraries:

@scene.on_frame      # register as a frame callback
@app.route('/home')  # register as a web route (Flask)
@staticmethod        # mark as a static method

In every case the idea is the same: instead of calling a registration function manually, you place @something above your function and Python does the registration for you.

For now, all you need to remember is: @scene.on_frame tells Python to call your function every frame.

{ “question_type”: “multiple_choice”, “question”: “What does the ‘@’ symbol do in ‘@scene.on_frame’?”, “options”: [ { “key”: “a”, “text”: “It marks the function as private” }, { “key”: “b”, “text”: “It automatically passes the function to scene.on_frame” }, { “key”: “c”, “text”: “It makes the function run once immediately” }, { “key”: “d”, “text”: “It is just a comment and has no effect” } ], “answer”: “b”, “submitted_answer”: “” }

Animating Multiple Objects

A single on_frame callback can animate as many objects as you like. The scene below has three spheres — each bounces at a different speed and phase, all updated inside one animate function.

Notice how each sphere uses a different speed and phase value pulled from a list, indexed by i inside a for loop.

import scene3d
import math

scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=12, width=8)

colors  = ['#e94560', '#f5a623', '#4488ff']
x_pos   = [-3.5, 0, 3.5]
speeds  = [1.5, 2.5, 3.8]
phases  = [0.0, 1.0, 2.2]

spheres = []
for i in range(3):
  s = scene3d.Shapes.Sphere(diameter=0.9, segments=14)
  s.set_color(colors[i])
  s.set_position(x_pos[i], 1, 0)
  scene.add(s)
  spheres.append(s)

t = 0.0

@scene.on_frame
def animate(dt):
  global t
  t += dt
  for i, sphere in enumerate(spheres):
    y = 1.0 + math.sin(t * speeds[i] + phases[i]) * 1.5
    sphere.set_position(x_pos[i], y, 0)

scene.run()

{ “question_type”: “true_false”, “question”: “You can only animate one object per on_frame callback — you need a separate callback for each object.”, “answer”: “False”, “submitted_answer”: “” }

Try It Yourself

Use the sliders to control the oscillation speed and amplitude. Watch how changing these values affects the motion — slower speed means longer, lazier sweeps; larger amplitude means a bigger swing.

import scene3d
import math

SPEED     = 2.0 #@param {type:"slider", min:0.5, max:6, step:0.5}
AMPLITUDE = 1.5 #@param {type:"slider", min:0.5, max:3, step:0.5}

scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=12, width=8)

sphere = scene3d.Shapes.Sphere(diameter=1, segments=16)
sphere.set_color('#e94560')
sphere.set_position(0, AMPLITUDE + 0.5, 0)
scene.add(sphere)

ctx = scene.get_context('2d')
t = 0.0

@scene.on_frame
def animate(dt):
  global t
  t += dt
  y = (AMPLITUDE + 0.5) + math.sin(t * SPEED) * AMPLITUDE
  sphere.set_position(0, y, 0)

  ctx.clear()
  ctx.fill_style = '#ffffff'
  ctx.font = '18px sans-serif'
  ctx.fill_text(f'speed={SPEED}  amplitude={AMPLITUDE}  t={t:.1f}s', 10, 24)

scene.run()

Think of a natural motion you’d like to recreate in 3D — a pendulum, ocean waves, a heartbeat pulse, a bird flapping.

Describe what the animation would look like and how you’d build it. What variables would you need? What math would produce the right motion? Would it use math.sin, a growing counter, or something else?