Scene Format¶
Skewer uses a layer-based JSON scene system. A scene consists of a manifest (scene.json) that references context and layer files, each defining geometry, materials, and render settings.
Overview¶
data/scenes/my_scene/
├── scene.json # Manifest: camera, context/layer refs, output_dir
├── ctx.json # Context: static scene-wide materials + objects
├── layer_bg.json # Layer: background objects
└── layer_fg.json # Layer: foreground objects
The manifest defines the camera and lists which files to load. Context files provide scene-wide shared content (lighting, sky, environment). Layer files define the objects to render, each with their own materials and geometry.
scene.json (Manifest)¶
The top-level file that orchestrates a render:
{
"camera": { ... },
"animation": { ... },
"context": ["ctx.json"],
"layers": ["layer_bg.json", "layer_fg.json"],
"output_dir": "renders/"
}
Quick Start
Quick Start: Use the sample scene template at apps/scene-previewer/public/templates/scene.json as a starting point. It includes a camera, context layer, and three object layers — just copy it into your scene directory and customize the values.
| Field | Type | Required | Description |
|---|---|---|---|
animation |
object | No | Animation configuration |
camera |
object | Yes | Camera configuration |
context |
string[] | No | Paths to context layer files |
layers |
string[] | Yes | Paths to render layer files (back-to-front order) |
output_dir |
string | No | Output directory (local path or gs:// URI). Empty = cwd |
skybox |
object | No | Skybox background (cube with textured faces) |
Animation¶
| Field | Type | Required | Description |
|---|---|---|---|
start |
number | Yes | Start time for animation (seconds) |
end |
number | Yes | End time for animation (seconds) |
fps |
number | Yes | Frames per second of animation |
shutter_angle |
number | Yes | The shutter period of each frame relative to the frame interval, defined as the arc-angle, in degrees of a semi-circle. See this article for more info |
Camera¶
"camera": {
"look_from": [0, 2, 5],
"look_at": [0, 0, 0],
"vup": [0, 1, 0],
"vfov": 60,
"aperture_radius": 0.05,
"focus_distance": 5.0,
"keyframes": [
{ "time": 0.0, "look_from": [0, 2, 5] },
{ "time": 2.0, "look_from": [3, 2, 5], "focus_distance": 3.0, "curve": "ease-in-out" }
],
"shutter_open": 0.0,
"shutter_close": 0.1
}
| Field | Type | Default | Description |
|---|---|---|---|
look_from |
Vec3 | — | Camera position in world space |
look_at |
Vec3 | — | Point the camera looks toward |
vup |
Vec3 | [0, 1, 0] |
Up vector for camera orientation |
vfov |
float | 90 |
Vertical field of view in degrees |
aperture_radius |
float | 0 |
Lens aperture radius. 0 = no depth of field; >0 enables DOF with focus_distance |
focus_distance |
float | 1.0 |
Distance from camera that is in focus (meaningful when aperture_radius > 0) |
keyframes |
array | — | Optional camera animation keyframes |
shutter_open |
float | 0 |
Time when shutter opens (animation time units). Non-zero enables motion blur |
shutter_close |
float | 0 |
Time when shutter closes. If equal to shutter_open, no motion blur |
Depth of Field: Set aperture_radius > 0 and focus_distance to the distance of your subject. Objects at focus_distance are sharp; objects closer or farther are progressively blurred.
Motion Blur: Set shutter_open and shutter_close to different values (e.g., 0.0 and 0.1).
Important
Do not set shutter_open and shutter_close for animated scenes. Instead, define the shutter_angle property in the animation configuration.
Camera keyframes are patch-based. Omitted fields carry forward from the previous camera keyframe,
seeded by the static camera fields. The animatable fields are look_from, look_at, vup,
vfov, aperture_radius, and focus_distance. When camera.keyframes has more than one entry,
all layers are frame-varying because the view changes even if the layer geometry is static.
Skybox¶
A skybox provides a textured background for the scene. When a camera ray misses all geometry, it hits the skybox instead of producing black or transparent pixels. The skybox does not affect reflected rays — it is a backdrop only.
"skybox": {
"center": [0, 0, 0],
"size": [100, 100, 100],
"faces": {
"+x": "textures/sky_posx.png",
"-x": "textures/sky_negx.png",
"+y": "textures/sky_posy.png",
"-y": "textures/sky_negy.png",
"+z": "textures/sky_posz.png",
"-z": "textures/sky_negz.png"
}
}
Either center+size or min+max defines the cube bounds. Exactly one pair is required.
| Field | Type | Required | Description |
|---|---|---|---|
center |
Vec3 | No* | Center of the skybox cube |
size |
Vec3 | No* | Extents of the cube (all components must be positive) |
min |
Vec3 | No* | Minimum corner (alternative to center+size) |
max |
Vec3 | No* | Maximum corner (alternative to center+size) |
faces |
object | Yes | Texture paths for each face, keyed by axis direction |
Face keys:
| Key | Direction |
|---|---|
"+x" |
Right |
"-x" |
Left |
"+y" |
Up |
"-y" |
Down |
"+z" |
Forward |
"-z" |
Backward |
Each face value is a path to a texture image (PNG, JPG, or EXR), resolved relative to the scene file directory. At least one face must be provided.
Behavior notes:
- Only camera (primary) rays sample the skybox. Reflected rays do not — they fall back to the environment (black if no emissive background). Use
skyboxfor clouds or backdrops that should appear behind the scene, not as an environment light. - When a skybox is present, setting
transparent_background: truestill produces alpha=1 for skybox pixels (they count as opaque). - Skybox pixels are written into the deep pixel buffer at their hit depth.
Layer/Context Files¶
Both context and layer files share the same structure:
{
"animated": true,
"materials": { ... },
"media": { ... },
"graph": [ ... ],
"render": { ... },
"visible": true
}
| Field | Type | Description |
|---|---|---|
animated |
bool | Defines if frames for this layer will be generated. Requires keyframes in the scene, along with the animation configuration to be set |
materials |
object | Named material definitions |
media |
object | Named volume medium definitions (NanoVDB) |
graph |
array | Scene graph — hierarchical tree of geometry nodes |
render |
object | Render options override (optional) |
visible |
bool | Layer-level visibility. If false, all materials become invisible but still cast shadows/reflections |
Materials¶
Materials are defined by name in the materials object. Each material has a type that determines its optical behavior.
Lambertian (Diffuse)¶
"materials": {
"matte_red": {
"type": "lambertian",
"albedo": [0.8, 0.2, 0.2],
"emission": [0, 0, 0],
"opacity": [1, 1, 1],
"visible": true,
"albedo_texture": "textures/brick_albedo.png",
"normal_texture": "textures/brick_normal.png"
}
}
Metal¶
"materials": {
"gold": {
"type": "metal",
"albedo": [1.0, 0.843, 0.0],
"roughness": 0.1,
"emission": [0, 0, 0],
"opacity": [1, 1, 1],
"visible": true,
"roughness_texture": "textures/brushed_metal_rough.png"
}
}
Dielectric (Glass / Transparent)¶
"materials": {
"glass": {
"type": "dielectric",
"albedo": [1.0, 1.0, 1.0],
"ior": 1.5,
"roughness": 0.0,
"emission": [0, 0, 0],
"opacity": [1, 1, 1],
"visible": true
}
}
Material Properties¶
| Property | Type | Default | Lambertian | Metal | Dielectric | Description |
|---|---|---|---|---|---|---|
type |
string | — | ✓ | ✓ | ✓ | lambertian, metal, or dielectric |
albedo |
Vec3 | [1,1,1] |
✓ | ✓ | ✓ | Base color (RGB, 0-1) |
emission |
Vec3 | [0,0,0] |
✓ | ✓ | ✓ | Emissive color (RGB, 0+). Use values >1 for HDR light sources |
opacity |
Vec3 | [1,1,1] |
✓ | ✓ | ✓ | Per-channel opacity (RGB, 0-1) |
visible |
bool | true |
✓ | ✓ | ✓ | Whether the material contributes to the image. Invisible materials still cast shadows and appear in reflections |
roughness |
float | 0 |
— | ✓ | ✓ | Microfacet roughness (0=mirror, 1=matte) |
ior |
float | — | — | — | ✓ | Index of refraction (1.5=glass, 1.33=water, 2.42=diamond) |
albedo_texture |
string | — | ✓ | ✓ | ✓ | Path to albedo/color texture map |
normal_texture |
string | — | ✓ | ✓ | ✓ | Path to normal map |
roughness_texture |
string | — | — | ✓ | ✓ | Path to roughness map |
Textures are resolved relative to the layer file's directory.
Emission makes a material a light source. Higher RGB values produce brighter light. Combine with a lambertian material and albedo: [0,0,0] for pure emissive lights (like the sun).
Scene Graph¶
The graph array defines a hierarchical tree of geometry. Each entry is a node that can contain children, forming a scene graph where transforms are inherited down the tree.
Node Types¶
Group¶
A container node that groups children and applies a transform to all of them:
{
"name": "earth_orbit",
"transform": {
"keyframes": [
{ "time": 0, "rotate": [0, 0, 0] },
{ "time": 1, "rotate": [0, 360, 0] }
]
},
"children": [
{
"type": "obj",
"file": "Earth.obj",
"name": "earth_body",
"transform": {
"translate": [42.5, 0, 0],
"scale": 4
}
}
]
}
| Field | Type | Description |
|---|---|---|
name |
string | Optional identifier for the node |
transform |
object | Static or animated transform (applied to all children) |
children |
array | Child nodes (groups, spheres, quads, or objs) |
Sphere¶
{
"type": "sphere",
"material": "glass",
"center": [0, 1, 0],
"radius": 0.5,
"visible": true,
"inside_medium": "fog",
"outside_medium": "air"
}
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Must be "sphere" |
material |
string | Yes | Material name (or "null"/"none" for no material) |
center |
Vec3 | Yes | Center position |
radius |
float | Yes | Radius |
visible |
bool | No | Per-object visibility override |
inside_medium |
string | No | Volume medium inside the sphere |
outside_medium |
string | No | Volume medium outside the sphere |
Quad¶
A four-vertex planar surface (useful for mirrors, light panels, floors):
{
"type": "quad",
"material": "mirror",
"vertices": [
[-2, 0, -2],
[ 2, 0, -2],
[ 2, 0, 2],
[-2, 0, 2]
],
"visible": true
}
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Must be "quad" |
material |
string | Yes | Material name |
vertices |
Vec3[4] | Yes | Four corner vertices in counter-clockwise order |
visible |
bool | No | Per-object visibility override |
OBJ Mesh¶
Loads a Wavefront .obj file:
{
"type": "obj",
"file": "models/teapot.obj",
"material": "ceramic",
"name": "teapot",
"auto_fit": true,
"visible": true,
"transform": {
"translate": [0, 0, 0],
"rotate": [0, 45, 0],
"scale": 2.0
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Must be "obj" |
file |
string | Yes | Path to .obj file (relative to scene directory) |
material |
string | No | Overrides all mesh materials with this one |
name |
string | No | Node identifier |
auto_fit |
bool | No (default true) |
Auto-scale and center the mesh to fit a unit bounding box |
visible |
bool | No | Per-object visibility override |
transform |
object | No | Static or animated transform |
Transforms¶
Transforms can be static (single TRS values) or animated (keyframes with interpolation).
Static Transform¶
| Field | Type | Default | Description |
|---|---|---|---|
translate |
Vec3 | [0, 0, 0] |
Position offset |
rotate |
Vec3 | [0, 0, 0] |
Rotation in degrees, applied as Euler angles (X, Y, Z) |
scale |
float or Vec3 | 1 |
Uniform scale (float) or per-axis scale (Vec3) |
Animated Transform (Keyframes)¶
"transform": {
"keyframes": [
{ "time": 0, "translate": [0, 0, 0], "rotate": [0, 0, 0] },
{ "time": 0.5, "translate": [5, 0, 0], "rotate": [0, 180, 0], "curve": "ease-in-out" },
{ "time": 1, "translate": [0, 0, 0], "rotate": [0, 360, 0] }
]
}
Each keyframe has:
| Field | Type | Required | Description |
|---|---|---|---|
time |
float | Yes | Time value (animation time units) |
translate |
Vec3 | No | Position at this keyframe. Accumulates from previous keyframe if omitted |
rotate |
Vec3 | No | Rotation at this keyframe (degrees). Accumulates from previous keyframe if omitted |
scale |
float or Vec3 | No | Scale at this keyframe. Accumulates from previous keyframe if omitted |
curve |
string or object | No (default "linear") |
Interpolation curve from this keyframe to the next |
TRS accumulation: When a keyframe omits a field, it inherits and continues from the previous keyframe's accumulated value. This means rotate: [0, 360, 0] at time 1 after rotate: [0, 180, 0] at time 0.5 produces continuous rotation, not a snap back.
Interpolation Curves¶
The curve field controls easing between keyframes:
| Preset | Description |
|---|---|
"linear" |
Constant speed (default) |
"ease-in" |
Slow start, fast end |
"ease-out" |
Fast start, slow end |
"ease-in-out" |
Slow start and end, fast middle |
Custom Bezier Curve¶
For fine-grained control, use a cubic Bezier with 4 control point values:
The array is [p1x, p1y, p2x, p2y] where P0=(0,0) and P3=(1,1) are fixed. This matches CSS cubic-bezier() syntax.
Transform Inheritance¶
Transforms compose down the scene graph. A child node's transform is applied after its parent's, so:
Animated group transforms create orbital motion: animate the group's rotation, and all children orbit around the group's origin.
Media (Volumes)¶
NanoVDB volumes add participating media (fog, smoke, clouds) to the scene:
"media": {
"fog": {
"type": "nanovdb",
"file": "volumes/cloud.vdb",
"sigma_a": [0.01, 0.01, 0.01],
"sigma_s": [0.9, 0.9, 0.9],
"g": 0.0,
"density_multiplier": 1.0,
"scale": 1.0,
"rotate": [0, 0, 0],
"translate": [0, 0, 0]
}
}
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | — | Must be "nanovdb" |
file |
string | — | Path to .vdb file |
sigma_a |
Vec3 | [0,0,0] |
Absorption coefficient (RGB) |
sigma_s |
Vec3 | [0,0,0] |
Scattering coefficient (RGB) |
g |
float | 0 |
Henyey-Greenstein phase function parameter (-1 to 1, 0=isotropic) |
density_multiplier |
float | 1.0 |
Scales all density values from the VDB |
scale |
float | 1.0 |
Spatial scale factor |
rotate |
Vec3 | [0,0,0] |
Rotation in degrees (Euler angles: X, Y, Z) |
translate |
Vec3 | [0,0,0] |
Spatial offset |
Use inside_medium / outside_medium on spheres to attach media to geometry:
{
"type": "sphere",
"material": "glass",
"center": [0, 0, 0],
"radius": 5.0,
"inside_medium": "fog"
}
Render Options¶
Each layer file can override render settings. The highest-priority render config wins (later layers override earlier ones):
"render": {
"integrator": "path_trace",
"max_samples": 1024,
"min_samples": 16,
"max_depth": 5,
"threads": 0,
"tile_size": 32,
"noise_threshold": 0.05,
"adaptive_step": 16,
"enable_deep": false,
"transparent_background": false,
"visibility_depth": 1,
"save_sample_map": false,
"image": {
"width": 1920,
"height": 1080,
"outfile": "output.png",
"exrfile": "output.exr"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
integrator |
string | "path_trace" |
"path_trace" for standard rendering, "normals" for normal visualization |
max_samples |
int | 200 |
Maximum samples per pixel |
min_samples |
int | 1 |
Minimum samples before adaptive convergence checks begin |
max_depth |
int | 50 |
Maximum ray bounce depth |
threads |
int | 0 |
Number of render threads. 0 = auto-detect (all available cores) |
tile_size |
int | 32 |
Tile dimension for work-stealing parallelism (NxN pixels) |
noise_threshold |
float | 0 |
Adaptive sampling convergence threshold. 0 = disabled (always render to max_samples) |
adaptive_step |
int | 16 |
Samples between convergence checks when adaptive sampling is enabled |
enable_deep |
bool | false |
Enable deep pixel buffers (for compositing) |
transparent_background |
bool | false (true when scene has >1 layer) |
Missed primary rays produce alpha=0 instead of black. Required for clean layer compositing |
visibility_depth |
int | 1 |
How many surface bounces to check for "covered" pixels when transparent_background=true. 1 = only direct camera visibility; higher values allow seeing visible objects through invisible surfaces |
save_sample_map |
bool | false |
Write per-pixel sample count heatmap (debug) |
image.width |
int | 800 |
Output image width in pixels |
image.height |
int | 450 |
Output image height in pixels |
image.outfile |
string | "output.png" |
Output PNG filename |
image.exrfile |
string | "output.exr" |
Output EXR filename (HDR) |
Adaptive Sampling¶
Set noise_threshold > 0 to enable adaptive sampling. Pixels that converge below the noise threshold stop sampling early, saving render time on simple regions while complex areas (edges, shadows, reflections) continue to max_samples.
This renders up to 4096 samples but stops per-pixel when noise drops below 0.02, checking every 32 samples after the first 64.
Transparent Backgrounds¶
For multi-layer compositing, set transparent_background: true:
- Rays that miss all geometry produce alpha=0 (transparent) instead of black
visibility_depthcontrols how many bounces are checked:1(default): only objects directly visible from the camera count2-4: allows visible objects seen through invisible surfaces (e.g., a visible sphere reflected in an invisible mirror)
Multi-layer scenes default to transparent_background: true
When a scene has more than one layer file, transparent_background defaults to true
automatically — even if not explicitly set — so layers can be composited together cleanly.
To render with an opaque black background, set "transparent_background": false in each
layer's render block:
With transparent_background: false, the camera background (black void or skybox) becomes
fully opaque (alpha=1), giving you a solid background in the final render.
Complete Example¶
A scene with an animated solar system:
scene.json:
{
"camera": {
"look_from": [0, 80, 120],
"look_at": [0, 0, 0],
"vfov": 45,
"aperture_radius": 0,
"focus_distance": 100
},
"context": ["ctx.json"],
"layers": ["layer_earth.json", "layer_mars.json"],
"output_dir": "renders/solar_system/"
}
ctx.json:
{
"materials": {
"sun_mat": {
"type": "lambertian",
"albedo": [0, 0, 0],
"emission": [2, 1.4, 0.6],
"opacity": [1, 1, 1],
"visible": true
},
"star": {
"type": "lambertian",
"albedo": [0, 0, 0],
"emission": [20, 20, 20],
"opacity": [1, 1, 1],
"visible": true
}
},
"graph": [
{
"type": "obj",
"file": "Sun.obj",
"material": "sun_mat",
"transform": { "scale": 13.5 }
},
{
"type": "sphere",
"material": "star",
"center": [-100, 50, -200],
"radius": 0.3
}
]
}
layer_earth.json:
{
"materials": {
"earth_surf": {
"type": "lambertian",
"albedo": [0.2, 0.4, 0.8],
"emission": [0, 0, 0],
"opacity": [1, 1, 1],
"visible": true
}
},
"graph": [
{
"name": "earth_orbit",
"transform": {
"keyframes": [
{ "time": 0, "rotate": [0, 0, 0] },
{ "time": 1, "rotate": [0, 360, 0] }
]
},
"children": [
{
"type": "obj",
"file": "Earth.obj",
"material": "earth_surf",
"transform": {
"translate": [42, 0, 0],
"scale": 4
}
}
]
}
],
"render": {
"integrator": "path_trace",
"max_samples": 1024,
"max_depth": 4,
"image": {
"width": 1920,
"height": 1080
}
}
}
See Also¶
- Rendering Tips - Best practices for quality and performance
- Animation - Keyframe animation and motion blur
- Mathematical Foundations - Rendering math and physics
- Compositing - Layer compositing with loom
- Blender Tools - Converting between Blender and Skewer formats
- Previewer - Visualizing scenes before rendering
- CLI Reference - Command-line options
- Architecture Overview - System design
- GCP Deployment - Cloud rendering setup