@oeave/bakery3 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +562 -2
- package/dist/animation-B1h0Ryvj.d.ts +97 -0
- package/dist/bake/index.d.ts +369 -0
- package/dist/bake/index.js +9 -0
- package/dist/bake/index.js.map +1 -0
- package/dist/bake-DZ-CJR6f.d.ts +1364 -0
- package/dist/catalog/index.d.ts +100 -0
- package/dist/catalog/index.js +154 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
- package/dist/chunk-3G5QL4F4.js +145 -0
- package/dist/chunk-3G5QL4F4.js.map +1 -0
- package/dist/chunk-4E5VV4QY.js +12 -0
- package/dist/chunk-4E5VV4QY.js.map +1 -0
- package/dist/chunk-62M6XXNX.js +142 -0
- package/dist/chunk-62M6XXNX.js.map +1 -0
- package/dist/chunk-6FYI6AJM.js +1157 -0
- package/dist/chunk-6FYI6AJM.js.map +1 -0
- package/dist/chunk-6NYR73Y7.js +832 -0
- package/dist/chunk-6NYR73Y7.js.map +1 -0
- package/dist/chunk-APWUCEEB.js +118 -0
- package/dist/chunk-APWUCEEB.js.map +1 -0
- package/dist/chunk-HNMSWQU7.js +1709 -0
- package/dist/chunk-HNMSWQU7.js.map +1 -0
- package/dist/chunk-JFYUDERE.js +1412 -0
- package/dist/chunk-JFYUDERE.js.map +1 -0
- package/dist/chunk-KNUAOILG.js +551 -0
- package/dist/chunk-KNUAOILG.js.map +1 -0
- package/dist/chunk-KZLVTSBI.js +191 -0
- package/dist/chunk-KZLVTSBI.js.map +1 -0
- package/dist/chunk-LAKXC4WR.js +2028 -0
- package/dist/chunk-LAKXC4WR.js.map +1 -0
- package/dist/chunk-NXBAZGNB.js +1107 -0
- package/dist/chunk-NXBAZGNB.js.map +1 -0
- package/dist/chunk-QRCT5UGZ.js +3286 -0
- package/dist/chunk-QRCT5UGZ.js.map +1 -0
- package/dist/chunk-S4AJTLLN.js +23 -0
- package/dist/chunk-S4AJTLLN.js.map +1 -0
- package/dist/chunk-UWBP7B54.js +92 -0
- package/dist/chunk-UWBP7B54.js.map +1 -0
- package/dist/chunk-XK35ANPJ.js +346 -0
- package/dist/chunk-XK35ANPJ.js.map +1 -0
- package/dist/chunk-Z7IYVH22.js +4781 -0
- package/dist/chunk-Z7IYVH22.js.map +1 -0
- package/dist/chunk-ZEBVAIJJ.js +156 -0
- package/dist/chunk-ZEBVAIJJ.js.map +1 -0
- package/dist/devtools/index.d.ts +536 -0
- package/dist/devtools/index.js +15 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/environments/index.d.ts +93 -0
- package/dist/environments/index.js +382 -0
- package/dist/environments/index.js.map +1 -0
- package/dist/hotspots/index.d.ts +111 -0
- package/dist/hotspots/index.js +285 -0
- package/dist/hotspots/index.js.map +1 -0
- package/dist/index.d.ts +975 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +5240 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +4927 -0
- package/dist/node/index.d.ts +597 -0
- package/dist/node/index.js +1328 -0
- package/dist/node/index.js.map +1 -0
- package/dist/prepare-BtjY4G3q.d.ts +112 -0
- package/dist/presets/index.d.ts +562 -0
- package/dist/presets/index.js +14 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/r3f/index.d.ts +159 -0
- package/dist/r3f/index.js +592 -0
- package/dist/r3f/index.js.map +1 -0
- package/dist/room-FS26KAPQ.js +9 -0
- package/dist/room-FS26KAPQ.js.map +1 -0
- package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
- package/dist/session-VCIQEO26.js +9 -0
- package/dist/session-VCIQEO26.js.map +1 -0
- package/dist/shapes/index.d.ts +222 -0
- package/dist/shapes/index.js +836 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/testRun-20OARnQr.d.ts +1139 -0
- package/dist/timeline-ChwgD7bT.d.ts +470 -0
- package/dist/tsl/index.d.ts +165 -0
- package/dist/tsl/index.js +310 -0
- package/dist/tsl/index.js.map +1 -0
- package/package.json +170 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oeave
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,563 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oeave/bakery3
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Path trace your Three.js scene.** Turn the scene you're already rendering
|
|
4
|
+
into a studio-quality image.
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="https://cdn.bakery3.com/sdk/readme/before-after-258272869e.gif" width="720" alt="Three rooms, each shown first as the Three.js viewport draws it and then as the same scene rendered by Bakery3">
|
|
8
|
+
</p>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<sub>Each room as the Three.js viewport draws it, then the same scene rendered by Bakery3.</sub>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @oeave/bakery3
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const { render } = useBakery3({ apiKey: 'bk_sk_...' });
|
|
19
|
+
|
|
20
|
+
const job = await render({ quality: 'studio' });
|
|
21
|
+
const image = await job.result();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No 3D-suite workflow.
|
|
25
|
+
No manual export.
|
|
26
|
+
No scene recreation.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
2048 × 2048
|
|
30
|
+
7 sec
|
|
31
|
+
$0.12
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- `three` r171 or newer is recommended, and every entry works with it. Older
|
|
39
|
+
releases work for some entries: r160 for the core, `/r3f`, `/node`, `/bake`,
|
|
40
|
+
`/devtools` and `/hotspots`, r161 for `/presets` and `/catalog`, and r170
|
|
41
|
+
for `/tsl`. `/environments` and `/shapes` need r171.
|
|
42
|
+
- `react` 18+ and `@react-three/fiber` 8.18+, only if you import `/r3f`.
|
|
43
|
+
- Node 20+ for `/node`.
|
|
44
|
+
- An account at [bakery3.com](https://bakery3.com) to render. The SDK asks for
|
|
45
|
+
a key only when it first calls the API, so `check()`, `inspect()` and
|
|
46
|
+
`capture()` work without one.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
**1. Get a key.** Sign up at [bakery3.com](https://bakery3.com), confirm your
|
|
51
|
+
email address and create a secret key on the
|
|
52
|
+
[Keys page](https://bakery3.com/keys). It starts with `bk_sk_`. Confirming the
|
|
53
|
+
address also adds $2 of free credit.
|
|
54
|
+
|
|
55
|
+
**2. Render.** Pass the key straight to the SDK. This is the fastest way to
|
|
56
|
+
see your own scene rendered, and it is fine on your own machine. Before the
|
|
57
|
+
page goes live, switch to a token: see
|
|
58
|
+
[Taking it to production](#taking-it-to-production).
|
|
59
|
+
|
|
60
|
+
### React Three Fiber
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Canvas } from '@react-three/fiber';
|
|
64
|
+
import { Bakery3Devtools, useBakery3 } from '@oeave/bakery3/r3f';
|
|
65
|
+
|
|
66
|
+
const apiKey = 'bk_sk_...'; // from bakery3.com/keys
|
|
67
|
+
|
|
68
|
+
function RenderButton() {
|
|
69
|
+
const { render, isRendering, result } = useBakery3({ apiKey });
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<button
|
|
74
|
+
disabled={isRendering}
|
|
75
|
+
onClick={() => render({ quality: 'studio' })}
|
|
76
|
+
>
|
|
77
|
+
Create studio render
|
|
78
|
+
</button>
|
|
79
|
+
{result && <img src={result.url} alt="Studio render" />}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function App() {
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<Canvas>
|
|
88
|
+
<Scene />
|
|
89
|
+
</Canvas>
|
|
90
|
+
<RenderButton />
|
|
91
|
+
<Bakery3Devtools apiKey={apiKey} />
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Put the button next to your `<Canvas>`, not inside it. The hook finds the
|
|
98
|
+
scene in your canvas and reads its camera, materials, lights, environment and
|
|
99
|
+
color settings. You don't pass any of them. `<Bakery3Devtools>` adds a panel
|
|
100
|
+
in the corner with the price of a render before you start it, and every
|
|
101
|
+
render your button starts.
|
|
102
|
+
|
|
103
|
+
With several canvases on one page, put a `<Bakery3Provider apiKey="…">` inside
|
|
104
|
+
the one to render, and call `useBakery3()` and `<Bakery3Devtools />` without
|
|
105
|
+
options anywhere on the page.
|
|
106
|
+
|
|
107
|
+
### Vanilla Three.js
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createBakery3 } from '@oeave/bakery3';
|
|
111
|
+
|
|
112
|
+
const bakery3 = createBakery3({ apiKey: 'bk_sk_...' });
|
|
113
|
+
bakery3.attach({ scene, camera, renderer });
|
|
114
|
+
|
|
115
|
+
const job = await bakery3.render({ quality: 'studio' });
|
|
116
|
+
const result = await job.result();
|
|
117
|
+
|
|
118
|
+
console.log(result.url);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`attach()` connects your scene once. Passing `renderer` makes the render use
|
|
122
|
+
your tone mapping and color space, so it is as bright as your viewport.
|
|
123
|
+
|
|
124
|
+
## Taking it to production
|
|
125
|
+
|
|
126
|
+
In production the key should stay on your server, and the browser gets a
|
|
127
|
+
short-lived token from it instead. That is one endpoint and one changed line.
|
|
128
|
+
|
|
129
|
+
**1. A token endpoint** on your server, with the key in an environment
|
|
130
|
+
variable:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// app/api/bakery3-token/route.ts
|
|
134
|
+
import { Bakery3Server } from '@oeave/bakery3/node';
|
|
135
|
+
|
|
136
|
+
const bakery3 = new Bakery3Server(process.env.BAKERY3_SECRET_KEY!);
|
|
137
|
+
|
|
138
|
+
export async function POST() {
|
|
139
|
+
const token = await bakery3.tokens.create({
|
|
140
|
+
expiresInSeconds: 300,
|
|
141
|
+
maxRenders: 1,
|
|
142
|
+
maxCost: 1,
|
|
143
|
+
allowedOrigins: ['https://shop.example.com'],
|
|
144
|
+
});
|
|
145
|
+
return Response.json(token);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A token expires, starts at most `maxRenders` renders, spends at most `maxCost`
|
|
150
|
+
dollars and only works from the origins you list. It can still follow and
|
|
151
|
+
cancel the renders it started for 24 hours after that, so a page sees its one
|
|
152
|
+
render through to the end. See
|
|
153
|
+
[bakery3.com/docs/auth](https://bakery3.com/docs/auth) for every option.
|
|
154
|
+
|
|
155
|
+
**2. Swap `apiKey` for `token`.** The SDK calls it whenever it needs a fresh
|
|
156
|
+
token:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const getToken = () =>
|
|
160
|
+
fetch('/api/bakery3-token', { method: 'POST' }).then((r) => r.json());
|
|
161
|
+
|
|
162
|
+
useBakery3({ token: getToken }); // React Three Fiber
|
|
163
|
+
createBakery3({ token: getToken }); // vanilla Three.js
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`<Bakery3Provider token={getToken}>` and `<Bakery3Devtools token={getToken} />`
|
|
167
|
+
take it the same way. The SDK warns in the console while a secret key is in
|
|
168
|
+
browser code.
|
|
169
|
+
|
|
170
|
+
`render()` captures a live scene, so it runs in the browser. To render from a
|
|
171
|
+
server or a script, give `renderBatch()` from `@oeave/bakery3/node` the URLs of
|
|
172
|
+
your `.glb` files: see [A catalog from your server](#a-catalog-from-your-server).
|
|
173
|
+
|
|
174
|
+
## Entry points
|
|
175
|
+
|
|
176
|
+
Each part has its own import path, so an app only bundles what it uses.
|
|
177
|
+
|
|
178
|
+
| Import | What it is |
|
|
179
|
+
| ----------------------------- | ------------------------------------------------------------ |
|
|
180
|
+
| `@oeave/bakery3` | `createBakery3()`: render, video, bake, estimate, check |
|
|
181
|
+
| `@oeave/bakery3/r3f` | `<Bakery3Provider>`, `useBakery3()`, `<Bakery3Devtools>` |
|
|
182
|
+
| `@oeave/bakery3/node` | `Bakery3Server`: browser tokens, webhooks, catalogs by URL |
|
|
183
|
+
| `@oeave/bakery3/bake` | Apply, compare and revert baked light on live meshes |
|
|
184
|
+
| `@oeave/bakery3/presets` | Rooms, pedestals, PBR materials and HDR environments |
|
|
185
|
+
| `@oeave/bakery3/environments` | Shader-lit backdrops whose glow is real light in the render |
|
|
186
|
+
| `@oeave/bakery3/shapes` | Abstract shapes, finishes and motions for creative coding |
|
|
187
|
+
| `@oeave/bakery3/tsl` | Renderer-only TSL nodes: noise, Voronoi, traced AO, bevel |
|
|
188
|
+
| `@oeave/bakery3/hotspots` | Clickable markers on a model, left out of every render |
|
|
189
|
+
| `@oeave/bakery3/catalog` | A whole catalog (products × finishes × cameras) as one batch |
|
|
190
|
+
| `@oeave/bakery3/devtools` | The floating Render · Video · Bake panel, without React |
|
|
191
|
+
|
|
192
|
+
## Progressive previews
|
|
193
|
+
|
|
194
|
+
If your app is user facing, don't make them stare at "Rendering…".
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const job = await render();
|
|
198
|
+
|
|
199
|
+
job.on('preview', ({ url }) => setImage(url)); // ~2s, low sample
|
|
200
|
+
job.on('progress', ({ percent }) => setProgress(percent));
|
|
201
|
+
job.on('complete', ({ result }) => setImage(result.url)); // ~8s
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Listening is enough: the job starts following the render on the first `on()`,
|
|
205
|
+
and a listener added late still gets the latest preview and progress.
|
|
206
|
+
`job.stop()` stops following it on this page. `job.cancel()` stops the render
|
|
207
|
+
itself.
|
|
208
|
+
|
|
209
|
+
The URLs in a result are signed and work for 24 hours (`result.expiresAt`).
|
|
210
|
+
Copy the file if you keep it. `bakery3.getRender(id)` gives you the render
|
|
211
|
+
again, with fresh URLs, after a reload or from a webhook.
|
|
212
|
+
|
|
213
|
+
## Know the cost before you spend it
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const estimate = await bakery3.estimate({
|
|
217
|
+
quality: 'studio',
|
|
218
|
+
width: 2048,
|
|
219
|
+
height: 2048,
|
|
220
|
+
});
|
|
221
|
+
// {
|
|
222
|
+
// estimatedCost: 0.12,
|
|
223
|
+
// maximumCost: 0.16,
|
|
224
|
+
// estimatedSeconds: 7,
|
|
225
|
+
// compatibilityScore: 0.97,
|
|
226
|
+
// }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`maxCost` is enforced: a render that could cost more is refused before any
|
|
230
|
+
GPU work starts, so nothing is charged.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
await render({ maxCost: 0.14 });
|
|
234
|
+
// RenderBudgetExceeded: This render could cost up to $0.16,
|
|
235
|
+
// which exceeds maxCost $0.14 (estimated $0.12)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
See [bakery3.com/docs/cost](https://bakery3.com/docs/cost).
|
|
239
|
+
|
|
240
|
+
## Check compatibility locally, for free
|
|
241
|
+
|
|
242
|
+
Runs in your browser. No account, no upload, no cost.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const report = bakery3.check();
|
|
246
|
+
// { score: 0.97, issues: [...], counts: { warning: 1, ... } }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Anything the renderer cannot reproduce faithfully comes back as a named,
|
|
250
|
+
located warning with a fix, never as a silently gray object:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
Unsupported material
|
|
254
|
+
|
|
255
|
+
Name: CarPaintFlakes
|
|
256
|
+
Type: ShaderMaterial
|
|
257
|
+
Location: <Car>/<Body>/<Paint>
|
|
258
|
+
|
|
259
|
+
Why: Arbitrary GLSL cannot currently be translated.
|
|
260
|
+
Fix: Add a MeshPhysicalMaterial render fallback.
|
|
261
|
+
Docs: https://bakery3.com/docs/material-fallbacks
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Declare what the renderer should use instead:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { Bakery3Material } from '@oeave/bakery3/r3f';
|
|
268
|
+
|
|
269
|
+
<Bakery3Material
|
|
270
|
+
web={<CarPaintShader />}
|
|
271
|
+
render={
|
|
272
|
+
<meshPhysicalMaterial
|
|
273
|
+
color="#830000"
|
|
274
|
+
metalness={0.9}
|
|
275
|
+
roughness={0.14}
|
|
276
|
+
clearcoat={1}
|
|
277
|
+
/>
|
|
278
|
+
}
|
|
279
|
+
/>;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`web` is a material element, or a component that forwards its ref to one. The
|
|
283
|
+
SDK warns in the console when it does not. Without React, set
|
|
284
|
+
`material.userData.bakery3 = { fallback: { … } }` with the same properties.
|
|
285
|
+
|
|
286
|
+
Every error the SDK throws prints its fix: an uncaught one shows the message,
|
|
287
|
+
why it happened, the fix and a docs link in your console.
|
|
288
|
+
|
|
289
|
+
## Render a video from a timeline
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
bakery3.attach({ scene, camera, renderer });
|
|
293
|
+
|
|
294
|
+
const tl = bakery3.timeline({ duration: 5, fps: 30 });
|
|
295
|
+
|
|
296
|
+
tl.push({ object: 'camera', orbitY: 90 }); // swing 90° around
|
|
297
|
+
tl.to(
|
|
298
|
+
mesh,
|
|
299
|
+
{ color: '#ff2d95' },
|
|
300
|
+
{ at: 1, duration: 2, easing: 'ease-in-out' },
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
tl.seek(2.5); // your scene IS the preview
|
|
304
|
+
const job = await bakery3.renderVideo({
|
|
305
|
+
timeline: tl,
|
|
306
|
+
format: 'mp4',
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`seek()` drives your actual Three.js objects, so what you scrub to is what
|
|
311
|
+
gets rendered. Already animating with gsap or Theatre.js? `timelineFromGsap()`
|
|
312
|
+
samples it. See [bakery3.com/docs/animation](https://bakery3.com/docs/animation).
|
|
313
|
+
|
|
314
|
+
## Bake path-traced light onto your live meshes
|
|
315
|
+
|
|
316
|
+
A render gives you a picture. A **bake** gives your running scene the light
|
|
317
|
+
from that picture: the SDK applies it to the meshes you already have, and
|
|
318
|
+
can take it off again.
|
|
319
|
+
|
|
320
|
+
<p align="center">
|
|
321
|
+
<img src="https://cdn.bakery3.com/sdk/readme/baked-room-0aa7175bc3.jpg" width="720" alt="The living_evening preset room in a live Three.js viewport, first with its own lights and then with its light baked">
|
|
322
|
+
</p>
|
|
323
|
+
<p align="center">
|
|
324
|
+
<sub>A preset room in a live Three.js viewport: with its own lights, then with its light baked.</sub>
|
|
325
|
+
</p>
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { createBakeSession } from '@oeave/bakery3/bake';
|
|
329
|
+
|
|
330
|
+
const job = await bakery3.bake({ scene, camera });
|
|
331
|
+
const { bundle } = await job.result();
|
|
332
|
+
|
|
333
|
+
const session = createBakeSession({ scene });
|
|
334
|
+
const generation = session.addGeneration(bundle);
|
|
335
|
+
await session.apply(generation); // lit
|
|
336
|
+
session.revert(); // exactly as it was
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Only the light is baked, so your textures, roughness and custom shaders keep
|
|
340
|
+
working, and reflections and clearcoat stay live on top. Name the objects you
|
|
341
|
+
bake (`mesh.userData.bakery3Id = 'pedestal'`) and a bake still fits after a
|
|
342
|
+
reload. See [bakery3.com/docs/bake](https://bakery3.com/docs/bake).
|
|
343
|
+
|
|
344
|
+
## Devtools
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
<>
|
|
348
|
+
<Canvas>
|
|
349
|
+
<Scene />
|
|
350
|
+
</Canvas>
|
|
351
|
+
{import.meta.env.DEV && <Bakery3Devtools apiKey="bk_sk_..." />}
|
|
352
|
+
</>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A floating **Render · Video · Bake** panel in the corner of your own app. It
|
|
356
|
+
shows the price next to every button before you press it, the compatibility
|
|
357
|
+
score, and a row with a cancel button for every running render, including the
|
|
358
|
+
ones your own `useBakery3()` button starts. What you made stays in a Recent
|
|
359
|
+
list, through reloads, until its links expire. A missing key or an empty
|
|
360
|
+
balance shows in the panel, with a link to fix it.
|
|
361
|
+
|
|
362
|
+
The Video tab films a timeline on your live camera: frame a shot and press
|
|
363
|
+
Keyframe camera. Pass `timeline={tl}` to film your own. Without React:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { mountBakery3Devtools } from '@oeave/bakery3/devtools';
|
|
367
|
+
|
|
368
|
+
const panel = mountBakery3Devtools({
|
|
369
|
+
bakery3, // the client from createBakery3()
|
|
370
|
+
scene,
|
|
371
|
+
camera,
|
|
372
|
+
renderer,
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
See [bakery3.com/docs/devtools](https://bakery3.com/docs/devtools).
|
|
377
|
+
|
|
378
|
+
## Several cameras, every variant
|
|
379
|
+
|
|
380
|
+
One capture, several cameras, the pictures back in the order you gave them:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const shots = await bakery3.render({
|
|
384
|
+
cameras: ['hero', 'front', 'detail'],
|
|
385
|
+
});
|
|
386
|
+
const [hero, front, detail] = await shots.result();
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
If a picture is refused, `result()` rejects with `RenderSetRefused`, which
|
|
390
|
+
names it and the reason, so the array never shifts. `shots.images()` still
|
|
391
|
+
walks the pictures that were accepted.
|
|
392
|
+
|
|
393
|
+
`renderVariants()` does that for a whole catalog. `apply` puts your scene in
|
|
394
|
+
each variant's state, the way a customer clicking would:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
const run = await bakery3.renderVariants({
|
|
398
|
+
key: 'fall-2026',
|
|
399
|
+
variants: products, // an array, a generator, an async cursor
|
|
400
|
+
apply: (product) => configurator.show(product),
|
|
401
|
+
cameras: ['hero', 'front', 'detail'],
|
|
402
|
+
maxCost: 600,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await run.wait();
|
|
406
|
+
for await (const image of run.images()) {
|
|
407
|
+
await save(image.variant, image.camera, image.url);
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Only one variant is in memory at a time, and running it again with the same
|
|
412
|
+
`key` picks up where it stopped. See
|
|
413
|
+
[bakery3.com/docs/batches](https://bakery3.com/docs/batches).
|
|
414
|
+
|
|
415
|
+
## A catalog from your server
|
|
416
|
+
|
|
417
|
+
When the models already sit on a CDN, no page is involved. From Node, with
|
|
418
|
+
your secret key:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { Bakery3Server } from '@oeave/bakery3/node';
|
|
422
|
+
|
|
423
|
+
const api = new Bakery3Server(process.env.BAKERY3_SECRET_KEY!);
|
|
424
|
+
|
|
425
|
+
const options = {
|
|
426
|
+
key: 'fall-2026',
|
|
427
|
+
products: [
|
|
428
|
+
{
|
|
429
|
+
id: 'chair-aria',
|
|
430
|
+
url: 'https://cdn.shop.com/models/chair-aria.glb',
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
shots: [
|
|
434
|
+
'three-quarter',
|
|
435
|
+
'front',
|
|
436
|
+
{ kind: 'video', from: 'front', to: 'back', seconds: 4 },
|
|
437
|
+
],
|
|
438
|
+
room: { preset: 'loft_golden' },
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Three products, every shot, small and fast
|
|
442
|
+
const test = await api.testBatch(options);
|
|
443
|
+
console.log(await test.result());
|
|
444
|
+
|
|
445
|
+
const run = await api.renderBatch({ ...options, maxCost: 600 });
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
On the Production and Scale plans. See
|
|
449
|
+
[bakery3.com/docs/catalog](https://bakery3.com/docs/catalog) and
|
|
450
|
+
[bakery3.com/docs/batches](https://bakery3.com/docs/batches).
|
|
451
|
+
|
|
452
|
+
## Webhooks
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { verifyWebhook } from '@oeave/bakery3/node';
|
|
456
|
+
|
|
457
|
+
const event = await verifyWebhook({
|
|
458
|
+
body: await request.text(), // the raw body, before parsing
|
|
459
|
+
signature: request.headers.get('bakery3-signature')!,
|
|
460
|
+
secret: process.env.BAKERY3_WEBHOOK_SECRET!,
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
See [bakery3.com/docs/webhooks](https://bakery3.com/docs/webhooks).
|
|
465
|
+
|
|
466
|
+
## Quality presets
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
quality: 'preview'; // fast and noisy, for iterating
|
|
470
|
+
quality: 'studio'; // the default
|
|
471
|
+
quality: 'ultra'; // for the hero image
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Leave out `width` and `height` and the long edge is the preset's (1024, 2048
|
|
475
|
+
or 4096) in your camera's shape, so a 16:9 viewport renders 16:9. Give one side
|
|
476
|
+
and the other follows the camera. `advanced: { samples, maxBounces, … }` is
|
|
477
|
+
there if you want it. See
|
|
478
|
+
[bakery3.com/docs/quality](https://bakery3.com/docs/quality).
|
|
479
|
+
|
|
480
|
+
## Preset rooms, pedestals and materials
|
|
481
|
+
|
|
482
|
+
A finished interior with furniture, plants and designed light, in one line:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
import { createPresetRoom } from '@oeave/bakery3/presets';
|
|
486
|
+
|
|
487
|
+
scene.add(createPresetRoom('living_evening'));
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Your page shows a light stand-in of the room, and the renderer rebuilds it at
|
|
491
|
+
full quality, so a render uploads only your product. The same entry has
|
|
492
|
+
pedestals (`createPedestal('pedestal_column')`), 4k PBR materials, a
|
|
493
|
+
parametric room and HDR environments. See
|
|
494
|
+
[bakery3.com/docs/presets](https://bakery3.com/docs/presets).
|
|
495
|
+
|
|
496
|
+
## What's supported
|
|
497
|
+
|
|
498
|
+
| | |
|
|
499
|
+
| -------------------- | ------------------------------------------------------------------------------------------- |
|
|
500
|
+
| **Cameras** | Perspective, orthographic |
|
|
501
|
+
| **Geometry** | Static meshes, instanced meshes, skinned meshes, morph targets |
|
|
502
|
+
| **Materials** | Standard and physical, with clearcoat, transmission, sheen |
|
|
503
|
+
| **Node materials** | Standard and physical node materials with TSL graphs |
|
|
504
|
+
| **Approximated** | Basic, Lambert, Phong, Toon, Matcap; Normal, Depth, Distance as flat gray (each warns once) |
|
|
505
|
+
| **Not translatable** | `ShaderMaterial`, `RawShaderMaterial`: declare a fallback |
|
|
506
|
+
| **Lighting** | Directional, point, spot, area, hemisphere, ambient, HDRI, IES |
|
|
507
|
+
| **Output** | PNG, JPEG, WebP, EXR. Transparent background by default |
|
|
508
|
+
|
|
509
|
+
The full list is at
|
|
510
|
+
[bakery3.com/docs/compatibility](https://bakery3.com/docs/compatibility).
|
|
511
|
+
|
|
512
|
+
## Custom shaders (TSL)
|
|
513
|
+
|
|
514
|
+
The SDK walks your TSL graph and the renderer rebuilds it node for node, so a
|
|
515
|
+
node material renders as it looks:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { positionLocal, mix, color, smoothstep } from 'three/tsl';
|
|
519
|
+
|
|
520
|
+
const material = new MeshStandardNodeMaterial();
|
|
521
|
+
material.colorNode = mix(
|
|
522
|
+
color('#7a1810'),
|
|
523
|
+
color('#e0e6ee'),
|
|
524
|
+
smoothstep(-0.2, 0.4, positionLocal.y),
|
|
525
|
+
);
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`@oeave/bakery3/tsl` adds nodes only the renderer can do exactly (fractal
|
|
529
|
+
noise, Voronoi, traced AO, bevel, curvature), with an approximate preview in
|
|
530
|
+
your viewport. See
|
|
531
|
+
[bakery3.com/docs/shader-nodes](https://bakery3.com/docs/shader-nodes).
|
|
532
|
+
|
|
533
|
+
## Privacy
|
|
534
|
+
|
|
535
|
+
Scenes and outputs are private by default, encrypted in transit and at rest,
|
|
536
|
+
retained briefly, deletable immediately, and never used to train anything.
|
|
537
|
+
`await bakery3.inspect()` returns exactly what a render would send. See
|
|
538
|
+
[bakery3.com/privacy](https://bakery3.com/privacy).
|
|
539
|
+
|
|
540
|
+
## Developing
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
bun install
|
|
544
|
+
bun test # unit tests, no network
|
|
545
|
+
bun run check-types
|
|
546
|
+
bun run build # dist/, one entry per import path
|
|
547
|
+
bun run check:three # every entry against the oldest three it promises
|
|
548
|
+
bun run format
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
`bun run check` runs the format check, the types and the tests together, and
|
|
552
|
+
`prepublishOnly` adds the build and the three.js check.
|
|
553
|
+
|
|
554
|
+
## License
|
|
555
|
+
|
|
556
|
+
The SDK is MIT licensed.
|
|
557
|
+
|
|
558
|
+
The preset library it loads is not part of that license. The HDRIs, six of the
|
|
559
|
+
materials and some of the models in the rooms come from
|
|
560
|
+
[Poly Haven](https://polyhaven.com) under CC0. The pedestals and the other
|
|
561
|
+
materials are ours. Some of the furniture and plants in the rooms are licensed
|
|
562
|
+
models: they appear in your renders and as a lighter stand-in in the viewport,
|
|
563
|
+
and are not available to download.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation: authored as keyframes, shipped as baked frames.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's `Timeline` is keyframes, easings and orbit segments; this is
|
|
5
|
+
* not. What travels is one exact value per output frame, and the renderer
|
|
6
|
+
* sets one keyframe per frame with linear interpolation, so there is nothing
|
|
7
|
+
* left for it to interpolate and nothing for the two sides to disagree
|
|
8
|
+
* about. Easing curves and orbit math exist in one implementation, and the
|
|
9
|
+
* browser preview and the traced frame agree on where everything is at
|
|
10
|
+
* frame N. It costs little: 10 s at 30 fps of camera motion is 300 frames of
|
|
11
|
+
* 7 floats.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* What a channel drives. The camera is not an object id: it may never have
|
|
15
|
+
* been in the scene graph at all (see `CameraSpec`). Neither is the
|
|
16
|
+
* environment: `scene.environment`, its intensity and rotation, and the
|
|
17
|
+
* scene's ambient light are scene state, not objects (see `EnvironmentSpec`).
|
|
18
|
+
*/
|
|
19
|
+
type AnimationTarget = {
|
|
20
|
+
type: 'camera';
|
|
21
|
+
} | {
|
|
22
|
+
type: 'object';
|
|
23
|
+
objectId: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'environment';
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* One animated property, sampled at every frame.
|
|
29
|
+
*
|
|
30
|
+
* `values` is flat: `frameCount * size` numbers, row-major, frame 0's
|
|
31
|
+
* components and then frame 1's. Nested arrays would cost two bytes of JSON
|
|
32
|
+
* per frame per channel, which for a long sequence is the larger part of
|
|
33
|
+
* the manifest.
|
|
34
|
+
*/
|
|
35
|
+
type AnimationChannel = {
|
|
36
|
+
target: AnimationTarget;
|
|
37
|
+
/**
|
|
38
|
+
* `position` | `quaternion` | `scale` | `fov` | `color` | `emissive` |
|
|
39
|
+
* `emissiveIntensity` | `roughness` | `metalness` | `opacity` | `intensity`
|
|
40
|
+
* | `target` | `groundColor`, and on the environment target the four in
|
|
41
|
+
* `ENVIRONMENT_CHANNEL_PROPERTIES`.
|
|
42
|
+
*
|
|
43
|
+
* `target` is a directional or spot light's aim, a WORLD point, as three's
|
|
44
|
+
* `light.target` holds it. Such a light is aimed from its position to its
|
|
45
|
+
* target at every frame, as three aims it, so animating either one turns
|
|
46
|
+
* the beam; `quaternion` on one is refused, because three ignores a
|
|
47
|
+
* directional or spot light's rotation.
|
|
48
|
+
*
|
|
49
|
+
* A property the renderer does not implement is refused with the name,
|
|
50
|
+
* the object's path and a fix, never dropped.
|
|
51
|
+
*/
|
|
52
|
+
property: string;
|
|
53
|
+
/** Components per frame: 3 position/scale/color, 4 quaternion, 1 scalar. */
|
|
54
|
+
size: number;
|
|
55
|
+
values: number[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The baked sequence.
|
|
59
|
+
*
|
|
60
|
+
* Frame `i` is sampled at `i / fps` seconds, so a `frameCount / fps` second
|
|
61
|
+
* clip never repeats its first pose at the end, which is what makes a 360
|
|
62
|
+
* degree orbit loop without a duplicated frame.
|
|
63
|
+
*/
|
|
64
|
+
type AnimationSpec = {
|
|
65
|
+
fps: number;
|
|
66
|
+
frameCount: number;
|
|
67
|
+
channels: AnimationChannel[];
|
|
68
|
+
/**
|
|
69
|
+
* Where the browser actually put things: see `WorldCheck`. Optional on the
|
|
70
|
+
* wire so an older worker reads straight through it; a worker that
|
|
71
|
+
* implements it refuses a clip whose frames disagree with the browser's.
|
|
72
|
+
*/
|
|
73
|
+
checks?: WorldCheck[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* A target's world matrix at a few of the clip's frames, straight off three
|
|
77
|
+
* (`Object3D.matrixWorld` after a seek; the camera's pose composed), in
|
|
78
|
+
* three's own Y-up frame, column-major, sixteen numbers per frame.
|
|
79
|
+
*
|
|
80
|
+
* The channels say what the browser did; this says where it ended up. The
|
|
81
|
+
* worker rebuilds every animated object from the channels through its own
|
|
82
|
+
* up-axis conversion and parent chain, then sets each of these frames, reads
|
|
83
|
+
* its own world matrices back, and refuses the job if they do not match the
|
|
84
|
+
* browser's: a refusal instead of a plausible clip with the motion in the
|
|
85
|
+
* wrong place. Five frames are enough (a convention error shows on the
|
|
86
|
+
* first, an axis error on any, a drift on the last) and cost eighty numbers
|
|
87
|
+
* per target.
|
|
88
|
+
*/
|
|
89
|
+
type WorldCheck = {
|
|
90
|
+
target: AnimationTarget;
|
|
91
|
+
/** Frame indices, ascending, each in `[0, frameCount)`. */
|
|
92
|
+
frames: number[];
|
|
93
|
+
/** `frames.length * 16` numbers. */
|
|
94
|
+
matrices: number[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type { AnimationSpec as A, AnimationChannel as a, AnimationTarget as b };
|