@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +562 -2
  3. package/dist/animation-B1h0Ryvj.d.ts +97 -0
  4. package/dist/bake/index.d.ts +369 -0
  5. package/dist/bake/index.js +9 -0
  6. package/dist/bake/index.js.map +1 -0
  7. package/dist/bake-DZ-CJR6f.d.ts +1364 -0
  8. package/dist/catalog/index.d.ts +100 -0
  9. package/dist/catalog/index.js +154 -0
  10. package/dist/catalog/index.js.map +1 -0
  11. package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
  12. package/dist/chunk-3G5QL4F4.js +145 -0
  13. package/dist/chunk-3G5QL4F4.js.map +1 -0
  14. package/dist/chunk-4E5VV4QY.js +12 -0
  15. package/dist/chunk-4E5VV4QY.js.map +1 -0
  16. package/dist/chunk-62M6XXNX.js +142 -0
  17. package/dist/chunk-62M6XXNX.js.map +1 -0
  18. package/dist/chunk-6FYI6AJM.js +1157 -0
  19. package/dist/chunk-6FYI6AJM.js.map +1 -0
  20. package/dist/chunk-6NYR73Y7.js +832 -0
  21. package/dist/chunk-6NYR73Y7.js.map +1 -0
  22. package/dist/chunk-APWUCEEB.js +118 -0
  23. package/dist/chunk-APWUCEEB.js.map +1 -0
  24. package/dist/chunk-HNMSWQU7.js +1709 -0
  25. package/dist/chunk-HNMSWQU7.js.map +1 -0
  26. package/dist/chunk-JFYUDERE.js +1412 -0
  27. package/dist/chunk-JFYUDERE.js.map +1 -0
  28. package/dist/chunk-KNUAOILG.js +551 -0
  29. package/dist/chunk-KNUAOILG.js.map +1 -0
  30. package/dist/chunk-KZLVTSBI.js +191 -0
  31. package/dist/chunk-KZLVTSBI.js.map +1 -0
  32. package/dist/chunk-LAKXC4WR.js +2028 -0
  33. package/dist/chunk-LAKXC4WR.js.map +1 -0
  34. package/dist/chunk-NXBAZGNB.js +1107 -0
  35. package/dist/chunk-NXBAZGNB.js.map +1 -0
  36. package/dist/chunk-QRCT5UGZ.js +3286 -0
  37. package/dist/chunk-QRCT5UGZ.js.map +1 -0
  38. package/dist/chunk-S4AJTLLN.js +23 -0
  39. package/dist/chunk-S4AJTLLN.js.map +1 -0
  40. package/dist/chunk-UWBP7B54.js +92 -0
  41. package/dist/chunk-UWBP7B54.js.map +1 -0
  42. package/dist/chunk-XK35ANPJ.js +346 -0
  43. package/dist/chunk-XK35ANPJ.js.map +1 -0
  44. package/dist/chunk-Z7IYVH22.js +4781 -0
  45. package/dist/chunk-Z7IYVH22.js.map +1 -0
  46. package/dist/chunk-ZEBVAIJJ.js +156 -0
  47. package/dist/chunk-ZEBVAIJJ.js.map +1 -0
  48. package/dist/devtools/index.d.ts +536 -0
  49. package/dist/devtools/index.js +15 -0
  50. package/dist/devtools/index.js.map +1 -0
  51. package/dist/environments/index.d.ts +93 -0
  52. package/dist/environments/index.js +382 -0
  53. package/dist/environments/index.js.map +1 -0
  54. package/dist/hotspots/index.d.ts +111 -0
  55. package/dist/hotspots/index.js +285 -0
  56. package/dist/hotspots/index.js.map +1 -0
  57. package/dist/index.d.ts +975 -0
  58. package/dist/index.js +15 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/node/index.cjs +5240 -0
  61. package/dist/node/index.cjs.map +1 -0
  62. package/dist/node/index.d.cts +4927 -0
  63. package/dist/node/index.d.ts +597 -0
  64. package/dist/node/index.js +1328 -0
  65. package/dist/node/index.js.map +1 -0
  66. package/dist/prepare-BtjY4G3q.d.ts +112 -0
  67. package/dist/presets/index.d.ts +562 -0
  68. package/dist/presets/index.js +14 -0
  69. package/dist/presets/index.js.map +1 -0
  70. package/dist/r3f/index.d.ts +159 -0
  71. package/dist/r3f/index.js +592 -0
  72. package/dist/r3f/index.js.map +1 -0
  73. package/dist/room-FS26KAPQ.js +9 -0
  74. package/dist/room-FS26KAPQ.js.map +1 -0
  75. package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
  76. package/dist/session-VCIQEO26.js +9 -0
  77. package/dist/session-VCIQEO26.js.map +1 -0
  78. package/dist/shapes/index.d.ts +222 -0
  79. package/dist/shapes/index.js +836 -0
  80. package/dist/shapes/index.js.map +1 -0
  81. package/dist/testRun-20OARnQr.d.ts +1139 -0
  82. package/dist/timeline-ChwgD7bT.d.ts +470 -0
  83. package/dist/tsl/index.d.ts +165 -0
  84. package/dist/tsl/index.js +310 -0
  85. package/dist/tsl/index.js.map +1 -0
  86. 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
- # Temporary Holding Version
1
+ # @oeave/bakery3
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 };