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()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.
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.
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 rateMultiplying 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 writex += speed * dtso 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_frameWhen 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 methodIn 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?