aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-28-generat3d-v1-design.md
blob: aac991f1c664b24146a9e68fbd5f84819bc9dbf0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# generat3D v1 design: DSON figure evaluator

Date: 2026-10-28. Status: approved in design session, pending spec review.

## Goal

Remove Daz Studio from a Linux pipeline of Daz content, Blender (Diffeomorphic
DAZ Importer), Cycles renders, Ren'Py. Today Diffeomorphic's most accurate
import mode ("Mesh Fitting: DBZ File") needs a `.dbz` file that only Daz
Studio can write, via Diffeomorphic's `export_to_blender.dsa`. generat3D
writes that file natively.

## Background

- The DSON format is publicly documented by Daz
  (`docs.daz3d.com/doku.php/public/dson_spec/start`).
- Diffeomorphic (GPLv2 or later) already reads DSON. It is reference reading
  only, not a dependency. Its formula evaluation is bound to Blender drivers.
- Clean-room rule: no reverse engineering of Daz Studio. Sources are the
  spec, Diffeomorphic's source, and reference `.dbz` files exported by Daz.
- Content license: renders of Daz content are covered by the standard
  license. The 3D data itself is never redistributed by this project.

## The `.dbz` format

Read from `to_daz_studio/Scripts/Diffeomorphic/export_to_blender.dsa`
(Diffeomorphic, commit 78afc2e) and `dbzfile.py`. Gzipped JSON. Top level:
content directory lists and `figures`, an array with one entry per scene
node, terminated by a `{"name": "dummy", "num verts": 0}` entry. A figure
entry has `name`, `id`, `label`, `center_point`, `end_point`, `orientation`,
`rotation_order`, `ws_transform`, `class`, the current-shape mesh (`num
verts`, `vertices`, `uvset`, `uvs`, `faces`, `material groups`) and `bones`,
each bone carrying the same node fields plus `origin`. Diffeomorphic treats
all of this as rest data; posing happens on the Blender armature.

Assumption to verify with corpus scene 6: the exported mesh is the morphed
rest shape, unaffected by pose. If false, v1 scope grows to include skinning
and this spec is revised.

The exact layout (key order, number formatting, which header keys
Diffeomorphic requires) is pinned down by diffing against corpus scene 1.

## Scope

v1 (tier 1): Genesis 9 figures. Base mesh, shape morphs, formula links
between dials (ERC), formula-driven joint centers (`center_point`,
`end_point`, orientation), limits. Output: the figure's `.dbz` entry.

Out of v1:

- Pose and skinning (Blender does it).
- Conforming clothing and hair fitting, auto-generated fit morphs (tier 2,
  own spec). Until then Diffeomorphic imports them in its approximate mode.
- HD morphs and geografts (tier 3, own spec).
- dForce, Iray materials (materials come from the `.duf` via Diffeomorphic).
- Genesis 8 and older: not targeted, not rejected. Revisit after v1.
- A debug OBJ export (declined: Blender import is the visual check).

## Architecture

```
scene.duf -> resolve figure .dsf via library roots -> base mesh + skeleton
          -> auto-load every .dsf in the figure's Morphs directories
          -> apply dial values from .duf -> property graph -> evaluate
          -> morphed rest vertices + bone rest data -> scene.dbz
```

Daz loads every morph found in a figure's Morphs directories when the figure
loads, whether or not the scene references it, because formulas in those
files can drive other properties. generat3D does the same.

Package `generat3d`:

| file | job | depends on |
|---|---|---|
| `library.py` | resolve DSON URIs (`/data/...dsf#id`, percent-encoded) across library roots in order, first match wins; read gzipped or plain JSON; cache | stdlib |
| `config.py` | load `config.toml`, validate | stdlib (`tomllib`) |
| `scene.py` | load `.duf`: figure instance, base geometry, skeleton, auto-discovered morphs, dial values | library |
| `formula.py` | property graph: operation stacks (push, add, mult, spline), sum and multiply stages, min/max clamps, topological evaluation, cycle detection | stdlib |
| `evaluate.py` | final property values to morphed vertices (numpy, sparse deltas) and bone rest data | scene, formula, numpy |
| `dbz.py` | write the `.dbz` in the export script's layout | evaluate |
| `__main__.py` | CLI | all |

`formula.py` knows nothing about meshes: property ids in, numbers out. Tiers 2
and 3 reuse `library`, `scene` and `formula` and add consumers next to
`evaluate`.

## CLI

```
python -m generat3d scene.duf [-L ROOT ...] [-o OUT.dbz]
```

- Output defaults to `scene.dbz` beside `scene.duf`: Diffeomorphic finds the
  `.dbz` by that name.
- `-L` is repeatable and searched in order. When given, it replaces the
  config roots (no merging).
- Exit codes: 0 ok, 1 error, 2 usage.
- Success prints nothing to stdout. Warnings and errors go to stderr.

## Config

`$XDG_CONFIG_HOME/generat3d/config.toml`, falling back to
`~/.config/generat3d/config.toml`:

```toml
library_roots = ["/home/you/Daz/My Library"]
```

- No roots from `-L` or config: fatal, `no library roots: pass -L or set
  library_roots in <path>`.
- A configured or `-L` root that does not exist: fatal.
- Unknown keys: fatal.
- Missing config file is fine when `-L` is given.
- `library_roots` is the only key in v1.

## Error handling

Policy: fail loudly. A wrong `.dbz` is worse than none, Diffeomorphic would
import it silently.

| situation | behavior |
|---|---|
| figure's base `.dsf` unresolved | fatal |
| morph used by the `.duf` with a nonzero value missing | collect all missing URIs, list them, fatal |
| unreadable file in an auto-loaded Morphs directory | warning, skip (Daz does the same) |
| unknown formula operation or stage | fatal, naming property id and file |
| cycle in the formula graph | fatal, printing the cycle; revisit if real content has legitimate cycles |
| malformed JSON or gzip | fatal, with file path |
| non-figure scene nodes (clothing, hair, props) | warning, not written |
| HD morphs or geografts present | warning, ignored |

The `.dbz` is written to a temporary file in the target directory and
renamed into place, so a failed run never leaves a partial file.

## Correctness

Ground truth is Daz itself: each corpus scene is exported to `.dbz` inside a
Windows VM with Diffeomorphic's export script. Each reference file records the
Daz Studio version that produced it.

Corpus (Genesis 9 Starter Essentials, free with Daz Studio), one feature per
scene:

| # | scene | proves |
|---|---|---|
| 1 | base G9, all dials 0 | URI resolution, geometry load, bone rest data, `.dbz` layout |
| 2 | one plain morph at 1.0, and a copy at 0.5 | delta application, linear scaling |
| 3 | a morph that drives others through formulas | formula engine, auto-loaded Morphs directories |
| 4 | a morph that moves joint centers | bone `center_point` and `end_point` formulas |
| 5 | several dials stacked, one past its limit | sum and multiply stages, clamps |
| 6 | scene 2, posed | rest-data assumption; expected identical to scene 2 |

Comparison: max absolute error per vertex coordinate and per bone value,
tolerance 1e-4 cm. A failure reports the worst offender and the count over
tolerance.

Acceptance for v1: all six scenes within tolerance, and one generated `.dbz`
imported by Diffeomorphic in Blender with DBZ mesh fitting, looking the same
as the Daz-exported one.

## Tests

stdlib `unittest`, run with `python -m unittest` from the repo root.

- Unit tests for `formula.py`, `library.py` and `config.py` on small DSON
  snippets written by hand. No Daz content, committed, always run.
- Reference tests diff generat3D output against `corpus/`. They skip with a
  notice when `corpus/` is absent.

## Repo layout

```
generat3d/                 the package
tests/                     unit and reference tests
fixtures/scenes/*.duf      committed scene recipes, URIs and dial values only
tools/export_corpus.dsa    batch export script, run inside the VM
corpus/                    gitignored, reference .dbz files from Daz
docs/superpowers/specs/    design specs
```

Reference `.dbz` files contain Daz geometry and are never committed.

## Reference VM

Throwaway Windows VM on the host, used only to produce the corpus.

- libvirt/KVM via `virt-install`, from a local Windows 10 22H2 ISO. Windows 10
  because Windows 11 needs a TPM and `swtpm` is not installed. No activation.
- Emulated SATA disk and e1000 NIC: no virtio ISO on the host, no guest
  drivers needed.
- Network on only to download Daz Studio, Install Manager and Genesis 9
  Starter Essentials. The user types the Daz credentials in the VM; the agent
  never reads them.
- Transfer: a second raw disk image, FAT32 formatted on the host. The VM
  writes the exports there; after shutdown the host loop-mounts it read-only
  and copies to `corpus/`. No network share, no SSH into Windows.
- `tools/export_corpus.dsa` opens each `.duf`, runs Diffeomorphic's export
  logic, and writes `.dbz` files to the transfer disk. Daz Studio accepts a
  script path on its command line, so the batch runs without clicking.

## Later

- Tier 2: conforming clothing and hair fitting.
- Tier 3: HD morphs and geografts.
- A Blender sidebar for library browsing and presets (own spec).
- Offer the evaluator upstream to Diffeomorphic once v1 is proven.