Writing scenes
A scene is one small TypeScript file. It says which body, clip, room and controls the scene has, and how the body reacts when you touch it. You need no build tool: DGen reads the file as it is.
Your scenes folder
Each profile has a scenes folder: scenes in your DGen folder by default. The Scenes tab of the config shows it, and open opens it. DGen writes into it:
| File | What it is |
|---|---|
my-scene.ts | An example scene, written once when the folder is new. Pick My scene in the scene list. |
dgen.d.ts | The API's types. DGen writes it again when a new version changes it. |
tsconfig.json | Maps dgen to those types, so your editor autocompletes every builder and value. Written once; yours after. |
AGENTS.md | A guide for a coding agent: the same text as the reference below. |
CLAUDE.md | One line that points Claude Code at AGENTS.md. Written once; yours after. |
Open the folder in an editor such as VS Code and start from my-scene.ts. Sounds go in a sounds folder beside your scenes.
Let your coding agent write it
Open a coding agent (Claude Code, Codex, Cursor) in your scenes folder and ask for the scene you want, in your own words. AGENTS.md tells it everything: the rules, every builder, the hooks, how to check a file. For example: "a scene where the body smiles when I stroke its arm and looks at my hand when it comes near the face".
Try a scene
- Save the file in your scenes folder.
- In the studio, pick it in the scene list (reload the page after an edit).
- If it fails, the error shows over the scene list with the file and the line.
The scene API
A scene file
- One
.tsfile in this folder is one scene. Its file name is its name in the page (my-scene.tsopens asscene=my-scene): lower case letters, digits and dashes. A name that is one of DGen's own scenes opens as~<name>, marked "(yours)". - Its one import is
import type { Dgen } from 'dgen'. It imports nothing at runtime: the server refuses any other import, naming the file and the line. Everything comes throughdgen. - Its default export is a function of
dgenthat returnsdgen.scene({...}). The builders only describe: nothing runs until the page opens the scene and its events fire. - Sounds are files of
sounds/in this folder.
The builders of dgen
| builder | what it gives |
|---|---|
scene | dgen.scene({ title, about, tags, body | bodies, ... }): the scene itself, what the file returns (its keys below) |
genesis9 | dgen.genesis9({ shape, anatomy, character, wear, at }): a Genesis 9 body from the user's Daz library. shape the body's sliders by key ({ size: 0.9, butt: 0.6 }, each in its range), anatomy one of the anatomy ids, character a character's id as the page's character list names it (one the user lacks falls back to plain Genesis 9), wear worn pieces as <item>:<colour> (its hair too), at where it stands, [x, z] or [x, z, heading] (m on the floor: x the room's left, z its front; the heading in deg, 0 facing +z, 90 facing +x), so two bodies on one clip stand apart; a couple's clip places its bodies itself and leaves it out |
wall | dgen.wall('breasts' | 'vulva', { layout, tint }): the region as a wall of copies, its layout one of the region's |
bar | dgen.bar({ x, z, height, radius }): a vertical steel bar on the floor at (x, z) of the body (m; x the body's left, z its front); the body meets it. At most two |
lock | dgen.lock('hand.L', [x, y, z]): one body of the skeleton held at its joint (a wrist, an ankle...) on the point, a cuff drawn there |
torso | dgen.torso(): the procedural torso, a body of its own (no clip, room or hooks) |
probe | dgen.probe(): a soft ball on a cone, a body of its own |
letters | dgen.letters({ look }): a word as soft flesh, its surface |
skin | dgen.skin(): a body's own skin, as a surface |
surface | dgen.surface('latex', { tint: [r, g, b] }): one of our materials in a colour (linear rgb, 0 to 1); dgen.surface('<item>:<colour>', { material, tint }): one material of a wardrobe colour |
choice | dgen.choice(id, ['a', 'b']) or dgen.choice(id, { a: valueA, b: valueB }): a control the user picks on the page, put where a value goes (its first option the default, def another) |
object | dgen.object('sphere' | 'cube' | 'cone', { size, at: [x, y, z] }), or { size, path: [{ at, t }, ...] } to move it on the clip's time (s): the page's one object |
sound | dgen.sound('file.ogg', { volume }): a sound file of sounds/ in this folder (mp3, ogg, opus, wav, flac, m4a), volume 0 to 1 |
line | dgen.line('file.ogg', { volume }): a spoken line, a sound of sounds/ with its lips track file.lips beside it (one row a phoneme: <start s> <end s> <viseme> <phoneme> <word>) |
on | dgen.on(event, filter?, (e, scene) => ...): a hook, its function run in the page when the event matches the filter (below) |
The keys of dgen.scene
| key | what it is |
|---|---|
title | its name in the scene list |
about | one line of what it is, 120 characters at most (the types ask for it; a scene without it still opens, its tile saying "no description"): the scene list shows it on hover, its filter matches it |
tags | the words that sort and find it, one at least (the types ask for them, as for about):physics (its flesh, skeleton or object is the point: it moves, swings, dents), interactive (its hooks answer a touch, a grab, a pointer near), couple (two bodies or more), wall (a region as a wall of copies), demo (a body that is not Genesis 9: a test of the solve), speech (a body speaks, its lips in sync), clothed (a body wears clothes), male (a masculine body), adult (adult content: nudity, genitals, a sexual act) |
body | the body: dgen.genesis9(), or torso, probe, letters |
bodies | two Genesis 9 bodies or more, in place of body (no wall or bars then) |
clip | the motion clip it plays, by its name in the page's clip list; one the user lacks plays their first clip, named over the stage. A couple's clip plays its part i on body i; none plays no clip: each body holds its pose, its flesh live |
room | the room: studio, dark, sky, plain |
camera | the camera's home: { yaw, pitch, dist, y } (rad, rad, m, m: the height it looks at) |
wall | dgen.wall(...), on one Genesis 9 body |
bars | at most two dgen.bar(...) |
locks | dgen.lock(...) each |
object | dgen.object(...) |
hooks | dgen.on(...) each |
music | a dgen.sound(...) looped from the first frame |
assets | the Daz products it uses beyond Genesis 9 Starter Essentials, by id, each 'mandatory' (the scene does not open without it, and says so) or 'optional' (it runs, that piece off): { 'expression-fusion': 'optional' }. The page lists the ones the user's library lacks under the scene's name, with where to get them. The ids: golden-palace (Golden Palace for Genesis 9: the female anatomy, its graft and its dials), dicktator (Dicktator for Genesis 9: the male anatomy, its graft and its dials), alicia (DEX Alicia 9: a character, its shape and skin), emphasized-visemes (Emphasized Visemes for Genesis 9: the mouth on a spoken line), breast-control (Breast Control for Genesis 9: the breast shape dials), expression-fusion (Expression Fusion: the face as it moves), eirgrid (Eirgrid Hair for Genesis 9: the default hair), chloe (CS Chloe Hair for Genesis 9: a second hair) |
Only title and body (or bodies) are needed. Clip, room, object, hooks and music are for Genesis 9 bodies.
Controls
A control is a value the user picks on the page: dgen.choice(id, options) put where the value goes, for example look: dgen.choice('surface', { flesh: dgen.skin(), latex: dgen.surface('latex', { tint: [0.6, 0, 0.1] }) }) on dgen.letters, or layout: dgen.choice('layout', ['anemone', 'spiral']) on a wall. The page draws each control the scene uses, under the scene's name, and keeps the pick in the url under its id. An id must be unique in the scene.
Hooks
dgen.on(event, filter, (e, scene) => ...). A touch event's filter may be left out (it then takes any touch).
| event | fires when |
|---|---|
touch | anything starts touching on |
press | a touch by the Press tool (or a VR finger) starts |
grab | a touch by the Grab tool starts |
pinch | a touch by the Pinch tool starts |
stroke | a press slides 4 cm along the skin since its touch or its last stroke |
release | the touch ends (let go, lifted, moved off); e.held how long (s), e.hard its most |
near | by comes within within m of on |
away | it goes past 1.25 times within again |
after | seconds after the scene opens, once |
every | each seconds, the first at seconds |
- Touch events (
touch,press,grab,pinch,stroke,release): filter{ on, by, tool }, each one value or a list.e:on,side(L,Ror undefined on the middle),by,tool,at(the point, m),hard(0 to 1),held(s). - Near events (
near,away): filter{ on, within, by },withinin m and needed.e:on,side,by,at,distance(m). - Time events (
after,every): filter{ seconds }, needed.e:time. on: a region of the body (head,neck,chest,breasts,waist,hips,butt,thighs,knees,calves,feet,upper arms,forearms,hands) orobject.by:pointer,left hand,right hand,object,bar. A VR hand fires what the mouse fires.tool:press,grab,pinch.
What a hook's function acts on, scene (the selected body):
| action | does |
|---|---|
emotion | scene.emotion('smile', { amount, ease, for }): one face to amount (1), the others to 0, eased over ease s (0.3); with for, back after that many s |
slider | scene.slider('eyesClosed', 1, { ease }): any slider of the body's shape, eased |
gaze | scene.gaze('camera' | 'off' | e.by | [x, y, z]): the eyes follow it (a pointer or a hand as it moves) |
music | scene.music(sound, { fade }): the one music, looped, crossfaded over fade s (1); null fades it out |
play | scene.play(sound): a sound once, over the music |
say | scene.say(line): the body says a dgen.line(...): its sound once and its mouth on the line's lips, in time |
clip | scene.clip('name'): another clip from its start; scene.clip('none') stops the clip, each body holding the pose it has |
after | scene.after(seconds, (scene) => ...): run later, on hook time |
time | scene.time: hook time, s since the scene opened |
A hook that throws shows its event and its message over the stage for 6 s; the other hooks go on.
What throws
The builders, or the page as it opens the scene, throw naming the value and the valid ones on: a slider out of its range, a worn piece that does not read, a place that is not two or three numbers, an unknown event, region, by or tool, a near hook with no within, a time hook with no seconds, an object size out of its range, a sound that is not a file of sounds/ or whose volume is not 0 to 1, a third bar, a control with no option or a def that is not one of them, an asset id that is not one of the ids or a need that is not mandatory or optional, an about empty, of two lines or past 120 characters, no tag or a tag that is not one of them. The page shows the error over the scene list.
Check a scene
- Types: this folder has a
tsconfig.json.bun checkhere (Bun 1.4 or later), ornpx tsc -p ., names each wrong value with its file and line, and nothing when every scene compiles. - The server:
curl -s http://localhost:<port>/api/scenes/<name>.js(the address DGen opened) answers the scene as JavaScript, or why it is refused (an import, a syntax error and its line). - The page: pick the scene in the scene list at the top. Reload the page after an edit.
Every body is an adult
A character whose name or product says young, teen, child or kid is refused, whatever its pack. Never write a scene that makes a body look young.
The example (my-scene.ts, written when the folder was new)
import type { Dgen } from 'dgen'
// a scene: one file of this folder, its name the scene's (lower case letters, digits and dashes). It imports nothing
// but the type of dgen; open the folder in an editor for its autocompletion. Pick it in the scene list at the top.
// Its hooks run when the body is touched; its sounds are files of sounds/ beside it: dgen.sound('my-music.mp3')
export default (dgen: Dgen) =>
dgen.scene({
title: 'My scene',
// what it is, for the scene list: one line, and the words that sort and find it
about: 'A body that smiles when stroked and is surprised when pinched',
tags: ['interactive', 'adult'],
body: dgen.genesis9({ shape: { size: 0.8, butt: 0.6 } }),
// the Daz products it uses beyond Genesis 9 Starter Essentials: an optional one it runs without (the grin
// below then keeps the face still), a mandatory one it does not open without; the page lists the ones your
// library lacks, with where to get them
assets: { 'expression-fusion': 'optional' },
hooks: [
// a smile for three seconds when the body is stroked (the Press tool, dragged along the skin)
dgen.on('stroke', (_, scene) => scene.emotion('smile', { for: 3 })),
// surprised at a pinch
dgen.on('pinch', (_, scene) => scene.emotion('surprised', { for: 1.5 })),
// a grin at a grab, a face of Expression Fusion
dgen.on('grab', (_, scene) => scene.emotion('ef10', { for: 2 })),
],
})