Skip to content

Cutscenes

A cutscene is a clip the whole scene plays, with a list of shots that say which camera it is watched through and when. Two shots that overlap fade from one camera to the other. It is saved as an .ocutscene file. orblit_motion reads and writes the file, and orblit_stage plays it over a staged scene.

pubspec.yaml
dependencies:
orblit_motion:
git:
url: https://github.com/ChxisB/orblit.git
path: packages/orblit_motion
orblit_stage:
git:
url: https://github.com/ChxisB/orblit.git
path: packages/orblit_stage

A CutsceneDocument has three parts:

  • motion is a ClipDocument that keys the scene. Its targets are the scene’s own ids. It keys no bones and carries no root motion, because a cutscene moves what is in the scene, not a model’s skeleton.
  • shots say which camera is watched when. A CutsceneShot names a camera entity by its id, with a start and a duration in seconds.
  • sounds say what should be heard when. A CutsceneSound names a sound by its path, with a start and a duration.

Its name, length, frame rate and whenDone are the clip’s.

import 'dart:io';
import 'package:orblit_motion/orblit_motion.dart';
import 'package:vector_math/vector_math_64.dart';
// The cube rises while the front camera watches. The side camera takes
// over, and the two blend for half a second.
final intro = CutsceneDocument(
motion: ClipDocument(
name: 'intro',
duration: 4,
channels: [
ClipChannel<Vector3>(
target: 'cube',
property: 'transform.position',
kind: ChannelKind.vector,
keys: [Key(0, Vector3.zero()), Key(4, Vector3(0, 2, 0))],
),
],
),
shots: [
CutsceneShot(camera: 'front', start: 0, duration: 2.5),
CutsceneShot(camera: 'side', start: 2, duration: 2),
],
sounds: [CutsceneSound(sound: 'sounds/swell.ogg', start: 0, duration: 4)],
);
void main() {
File('cutscenes/intro.ocutscene').writeAsStringSync(intro.encode());
final read = CutsceneDocument.decode(
File('cutscenes/intro.ocutscene').readAsStringSync(),
);
for (final problem in read.problems) {
print(problem);
}
}

shotsAt(seconds) says which cameras are watched at a moment, each with a weight. At 1 second this one is front alone. At 2.25 seconds it is front and side at 0.5 each, halfway through the overlap. At 3 seconds it is side alone. The weight eases in and out across an overlap rather than moving at a steady rate. Before the first shot, in a gap between shots and at the very end, the list is empty.

This is what encode writes for the cutscene above:

cutscenes/intro.ocutscene
{
"kind": "orblit.cutscene",
"formatVersion": 1,
"name": "intro",
"duration": 4.0,
"rate": 30.0,
"whenDone": "hold",
"shots": [
{"camera":"front","start":0.0,"duration":2.5},
{"camera":"side","start":2.0,"duration":2.0}
],
"sounds": [
{"sound":"sounds/swell.ogg","start":0.0,"duration":4.0}
],
"channels": [
{
"target": "cube",
"property": "transform.position",
"kind": "vector",
"keys": [
{"at":0.0,"value":[0.0,0.0,0.0]},
{"at":4.0,"value":[0.0,2.0,0.0]}
]
}
],
"marks": []
}

It is a clip’s file with shots and sounds added, laid out one key, shot or sound to a line. decode is as lenient as a clip’s. A shot that can’t be read is dropped and listed in problems, and so are keys on bones. A file with no duration lasts until its last shot or sound ends. A file that isn’t a cutscene, or was written by a newer Orblit, throws CutsceneFormatException.

OrblitCutscenes plays cutscenes on an OrblitDocumentView, one at a time:

import 'package:orblit_motion/orblit_motion.dart';
import 'package:orblit_stage/orblit_stage.dart';
class Cinematics {
Cinematics(OrblitDocumentView view, List<CutsceneDocument> cutscenes)
: cutscenes = OrblitCutscenes(view, cutscenes);
final OrblitCutscenes cutscenes;
bool get playing => cutscenes.playing != null;
// Once a frame, with the seconds since the last one.
void tick(double seconds) {
final step = cutscenes.advance(seconds);
for (final sound in step.sounds) {
print('${sound.sound} should be ${sound.at} seconds in');
}
if (step.ended) {
// Hand the player their controls back.
}
}
}

start(name) starts one by name. Each advance(seconds) keys the scene and sets view.through to the shots’ camera. Where two shots overlap, the view is between the two cameras: where they stand, which way they look and their field of view are averaged by weight. advance returns an OrblitCutsceneStep with the marks the cutscene passed, the sounds that should be playing, and whether it ended. At the end through is null again, so the view looks through the scene’s own camera. stop() ends one as though it had reached its end.

The clip’s whenDone says what happens to the scene at the end. hold, the default, leaves the scene where the cutscene put it. release puts back everything it moved.

A few things to know:

  • Hide the cutscene’s cameras. The scene’s own camera is the first camera entity that is shown. A shot looks through its camera whether it is shown or not, so a hidden one works and can’t become the game’s camera by accident.
  • Nothing is looked through between shots. Before the first shot, in a gap and at the very end, the view falls back to the scene’s own camera.
  • Orblit plays no sound. The step says which sounds should be playing and how far in, and the game plays them.
  • A cutscene runs on the scene itself, not on a copy. Keep the game’s own animators off what it keys while it runs.
  • Each name is used once. Two cutscenes with the same name throw an ArgumentError, since one of them could never be started.

A mark named after a cutscene starts it. Put the mark on any clip the game plays, and hand each step’s marks to startFrom:

import 'package:orblit_motion/orblit_motion.dart';
import 'package:orblit_stage/orblit_stage.dart';
// Once a frame. A clip that passes a mark called "intro" starts the
// cutscene called intro.
void tick(ClipPlayer player, OrblitCutscenes cutscenes, double seconds) {
cutscenes.startFrom(player.advance(seconds).marks);
final step = cutscenes.advance(seconds);
cutscenes.startFrom(step.marks);
}

A cutscene’s own marks come back in its step, so one cutscene can start the next. Starting one ends whichever was running.

The Cinematics workspace makes cutscenes. The scene view and the Shot panel sit side by side, the cutscene’s timeline is under them, and the Shots list and the inspector are on the right. The shelf has New cutscene, Add shot and Use this view.

  • New cutscene makes a four-second cutscene in cutscenes/ and opens it. Project › New › Cutscene makes one too, and double-clicking an .ocutscene file opens it here. A cutscene is named after its file when it is made.
  • Add shot cuts to the selected camera at the playhead, or to the scene’s first camera when no camera is selected. The shot running at the playhead ends there, and the new one holds until the next shot starts or the cutscene ends.
  • Use this view puts a new camera where the scene view is, looking where it looks, and cuts to it. The camera and the shot are a step each on the undo stack.

Each shot has a card in Shots. Choose its camera, type when it starts and how long it lasts, or delete it. The cards stay in the order the shots start.

Look through flies the scene view to a shot’s camera, and from then on the camera follows the view. Frame the shot by moving. Orbit, pan and zoom as usual, or hold the right button and fly with W, A, S and D. E or Space rises and Q sinks. The camera keeps no roll. Stop looking lets go, and so does leaving the workspace. Undo puts the camera back where it was.

The timeline is the one from Animation. It keys what the scene holds, from the inspector’s diamonds and on the dope sheet. Under its ruler, a Marks strip and a Shots strip show where the marks and shots fall. The Shot panel shows the finished shot at the playhead, through the same camera blend the game uses. Between shots it shows the scene’s own camera.

The top bar’s Play reads every cutscene in cutscenes/, open ones as they are now. When a clip passes a mark named after one, the cutscene starts, and the Game view looks through its shots until it ends. Two cutscenes with the same name get a warning in the log, and only the first plays.

  1. Add a cube and a second camera. The starter scene already has one.
  2. Open Cinematics and press New cutscene.
  3. Select the cube in the scene view. Key its position at the start, move the playhead to the end, raise the cube and key its position again.
  4. Put the playhead at the start and press Add shot. Move the playhead to 2 seconds and press Add shot again. Choose the second camera in the new card’s Camera menu.
  5. Press Look through on each card, frame the shot, and press Stop looking.
  6. In Animation, select an object and press Make a clip. Press the + beside Marks and name the mark cutscene, the new cutscene’s name.
  7. Press Play in the top bar. The clip passes the mark, and the Game view plays the cutscene.
  • Play sound. The file holds sounds and the step names them, but the game plays them, and the editor has no strip for them.
  • Move bones. A cutscene keys what is in the scene, not a model’s skeleton.