3D scenes

Write 3D in TSX with Bevy's names: a camera, lights, meshes and materials, moved by the frame, with 2D titles over them in the same composition.

View as Markdown

You write 3D the way you write the rest of a composition: components whose props are computed from the frame. raea turns them into Bevy entities (meshes, lights, cameras, glTF models, post-processing) and draws them under the page, so titles, captions and a score sit over the 3D in the same file. You never write Rust or a render loop. A render and a live play of the same file draw the same thing, so what you check in a render is what plays.

A turntable

turntable.tsx
// check: frames 0,45,89
import { AbsoluteFill, Composition, Easing, interpolate, registerRoot, useCurrentFrame, useVideoConfig } from "raea";
import { Cylinder, Entity } from "raea/3d";
import { StandardMaterial } from "raea/pbr";
import { Cuboid, Mesh3d } from "raea/mesh";
import { AmbientLight, DirectionalLight } from "raea/light";
import { Quat, Transform } from "raea/transform";
import { Camera3d } from "raea/camera";

function Turntable() {
  const f = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();
  const turn = (f / fps) * (Math.PI / 3); // a sixth of a turn a second
  const z = interpolate(f, [0, durationInFrames - 1], [7, 5], { easing: Easing.out(Easing.cubic), extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  return (
    <AbsoluteFill style={{ background: "#14161c" }}>
      <Camera3d transform={Transform.fromXyz(0, 1.6, z).lookingAt([0, 0.5, 0])} projection={{ Perspective: { fov: (40 * Math.PI) / 180 } }} />
      <AmbientLight brightness={160} />
      <DirectionalLight transform={Transform.fromXyz(3, 5, 4).lookingAt([0, 0, 0])} illuminance={11200} shadow_maps_enabled />
      <Entity transform={Transform.IDENTITY.withRotation(Quat.fromEuler("XYZ", 0, turn, 0))}>
        <Mesh3d transform={Transform.fromXyz(0, -0.1, 0)}>
          <Cylinder radius={1.4} half_height={0.1} />
          <StandardMaterial base_color="#2b2f3a" perceptual_roughness={0.6} />
        </Mesh3d>
        <Mesh3d transform={Transform.fromXyz(0, 0.6, 0)}>
          <Cuboid half_size={[0.45, 0.6, 0.45]} />
          <StandardMaterial base_color="#e8b04a" perceptual_roughness={0.35} metallic={0.2} />
        </Mesh3d>
      </Entity>
      <div style={{ position: "absolute", top: 80, width: "100%", textAlign: "center", fontFamily: "Inter", fontWeight: 800, fontSize: 72, color: "#ffffff" }}>
        Made to be seen
      </div>
    </AbsoluteFill>
  );
}

registerRoot(() => <Composition id="turntable" component={Turntable} width={1080} height={1080} fps={30} durationInFrames={90} />);
Terminal
raea render turntable.tsx --at 0,45,89 -o frames/
raea render turntable.tsx -o turntable.mp4

What each part does:

  • Camera3d is the camera: where it is (transform) and its lens (projection, with the field of view in radians). The first camera in the tree is the one that renders.
  • Lights: AmbientLight fills the shadows, DirectionalLight is a sun (its brightness in lux, here 11,200), shadow_maps_enabled turns its shadows on.
  • Entity groups parts so they move together. Turning the group turns the plinth and the box on it.
  • Mesh3d is a visible thing: a shape (Cylinder, Cuboid, Sphere, Torus and others from raea/mesh) and a material (StandardMaterial from raea/pbr, with base_color, perceptual_roughness and metallic).
  • Transform places things: Transform.fromXyz(x, y, z), .lookingAt([x, y, z]), and rotations from Quat.fromEuler. Y is up and units are metres, as in Bevy.
  • The div after the 3D is ordinary 2D layout, drawn over the scene.

The rules for 3D

  1. Every value comes from the frame. A spin is (frame / fps) * speed, not a value kept between frames. That is what keeps a render and a live play the same.
  2. Rotations are in radians. Quat.fromEuler("XYZ", 0, Math.PI / 2, 0) is a quarter turn about Y.
  3. Colours are hex strings. CSS variables don't reach the 3D.
  4. Name parts that come and go (<Mesh3d name="lid">), so raea matches them across frames.
  5. A part's shape is fixed for its life. Animate its transform, colour, opacity, emission, roughness or metalness; to change its shape, swap in another named part.

Checking a scene

Terminal
raea scene lint turntable.tsx          # parts floating, sunk into the floor, or clipping each other
raea scene light turntable.tsx         # how the scene is lit, and whether the subject is exposed well
raea render turntable.tsx --review review/   # fixed cameras (hero, front, right, top, iso, low) in colour and clay

--review writes the same scene from six fixed cameras, in colour and as untextured clay, so you can see the shapes and the light without the materials getting in the way.

The 3D guides

The 3D guides go topic by topic, each with a complete composition to start from. Read one or two at a time:

Terminal
raea guide bevy               # the map of 3D topics
raea guide bevy/lights        # one topic
TopicCovers
bevy/sceneThe camera, camera moves, groups, 2D under and over 3D
bevy/lightsSun, bulbs, spots, ambient light, shadows, brightness
bevy/materialsColour, metal, glass, glow, clearcoat, textures, toon shading and outlines, decals
bevy/meshesPrimitives, modelled solids, blended organic shapes, trees, terrain, rocks, 3D text
bevy/animationSpins, springs, reveals, model animation clips
bevy/charactersBodies posed by goals, motion capture, walking to a spot
bevy/roomsInteriors, furniture along walls, windows, daylight, sound placed in the scene
bevy/games3D games: state, input, many entities, collisions, state machines
bevy/assetsglTF models, pictures in space, where files go
bevy/blenderFixing an asset in Blender first: scale, pivot, heavy meshes, FBX to glTF
bevy/splatsGaussian splats trained from footage, and animating them
bevy/liquidsLiquids and goo, simulated and baked
bevy/postBloom, fog, tone mapping, exposure, lens effects, motion blur
bevy/limitsHow much a scene can hold, and what isn't there yet

The same topics ship in the raea skill as 3d/*.md.