⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
anyscene
Build a photoreal, film-style Blender scene from a one-paragraph brief. Generate concept images and every prop with image→3D models on fal, then script the layout, set dressing, grime, lighting, camera moves and grade. Use it when someone asks for a realistic 3D environment, a set or a cinematic shot of a place, whether starting fresh or extending an existing AnyScene scene.
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
Not yet listed on ClawHub or SkillsMP
// README
AnyScene
Give it reference images of a place (or a one-paragraph brief) and it builds a dressed, lit Blender scene: a real room with every prop as its own model, practical lights, grime, haze, cinematic camera moves and graded renders.
The work is done by an AI agent (Claude, in Claude Code) following SKILL.md. No prop is modelled by hand:
each one is generated as an image, turned into 3D with two image-to-3D models on fal, and the better
result is picked by comparing same-light renders.

Five places, one process
| Scene | ||
|---|---|---|
![]() | Electronics repair shop, Tokyo, 1 AM, rain outside | scenes/tokyo-electronics-repair · 86 generated props |
![]() | Ramen shop, Tokyo, last orders, steam and rain | scenes/tokyo-ramen-shop · 74 generated props |
![]() | Dai pai dong, Hong Kong, an open-air street kitchen just after rain | scenes/hk-dai-pai-dong · 78 generated props |
![]() | Mission control, 1970s, a night shift during an orbital flight | scenes/mission-control-1970s |
![]() | Maison Roussel, a Paris bakery. Built by the hosted endpoint from one reference image, with no human input, in under two hours | (endpoint run) |
The first four were built by an agent working through SKILL.md, with a person checking its gates. The bakery was built
end to end by the same process running unattended on a fal GPU worker.
This repo is the process and the code. The finished scenes (.blend files, generated models, textures, renders) add up
to tens of gigabytes and are not in git. Each scene folder keeps what's needed to rebuild it: its prop list
(assets.json), model picks with reasons (picks.json), shots (shots.json), notes and build scripts.
Quick start
You need:
- Blender 5.0 or newer. Set
BLENDERif it isn't on your PATH or in the default install location. - Node 20 or newer, then
npm installin the repo root. - A fal API key in
FAL_KEY. Generation is billed to your fal account: roughly $1.65 per prop (an image plus two 3D models), so about $150 for a 90-prop scene. - ffmpeg with an H.264 encoder (
FFMPEG, defaultffmpeg). The example scenes were built on Windows with an NVIDIA GPU and useh264_nvenc; on other machines, swap inlibx264. - uv for the Python helper scripts.
- Claude Code.
- A GPU with 12–16 GB of memory for Cycles.
Then open the repo in Claude Code and ask for a scene:
Use SKILL.md to build a new scene: a Lisbon tram depot at dawn, one tram half-stripped for repair.
Or start from your own references: put a few images (and, optionally, a floor plan) in refs/<scene>/ and say so:
Use SKILL.md to build a scene from the reference images in refs/lisbon-tram-depot/. Treat them as the approved concept.
Check the agent's work at each gate: concept, prop picks, clay layout, lit stills, shot previews. Expect most of a day of agent time and 1–2 GPU-hours of final renders per scene.
How the agent works
SKILL.md is the whole process, step by step, with every gotcha the example scenes hit:
- brief → scene spec (room size, zones, hero spots, real light sources);
- concept images, or your references;
- a prop manifest, then a reference image for every prop;
- each prop through both Tripo H3.1 and Meshy 7.1, picked by looking at same-light renders;
- a scripted room shell, dressing from big furniture down to small clutter, with collision and floating checks;
- grime, de-branding, decals, practical lighting, haze, steam and rain;
- cinematic and long handheld walkthrough shots, rendered in Cycles and graded;
- optionally, a behind-the-scenes video.
Every step ends with a gate: a contact sheet the agent has to look at before moving on. Everything is a script, so the .blend is a build output that can be regenerated.
builder/CRAFT.md is the short version of what made the good scenes good: render it and look, name
the problem precisely, fix it in code, check the fix from a view that shows it, and get an independent critic.
Layout
SKILL.md the process; agents start here
builder/CRAFT.md the review loop that separates good scenes from first drafts
generate/ fal client: images and image→3D (Tripo H3.1, Meshy 7.1), with a local spend log
pipeline/ Blender/ffmpeg discovery, a FIFO GPU queue, comparison renders, gallery pages, floating-prop check
camera/ camera rig and shot renderer: moves, rack focus, handheld walk, lens effects (synced from AnyShot)
breakdown/ behind-the-scenes videos, promo cuts and the launch supercut
scenes/<name>/ one folder per scene: assets.json, picks.json, shots.json, NOTES.md, brief.md, scripts/
farm/ a fal Serverless render farm for Cycles (app.py, synced from AnyShot), and a client that renders a scene's shots on it
blender_addon/ the fal Render Farm add-on for Blender
builder/ the same process packaged as a fal Serverless app (the hosted endpoint)
docs/ endpoint and API notes, README images
scripts/ sync_from_anyshot.py: refresh the files shared with AnyShot from a sibling ../anyshot checkout
Hosted endpoint (optional)
If you'd rather not set up Blender, a GPU and the toolchain, the hosted endpoint runs the whole build on fal: send one
to four reference images (and an optional prompt), and get back the scene package (scene.blend with every prop as
its own object, a scene.glb, stills and panoramas; walkthroughs render on the farm). The bakery above was built this way, from a single image.
The endpoint's code is in builder/ if you want to deploy your own copy to your fal account
(fal deploy builder/app.py::SceneBuilderApp --app-name anyscene-builder). It needs FAL_KEY and OPENROUTER_API_KEY set
as secrets on the app, and the endpoint/ folder alongside it (the builder imports its planner and schema).
Shooting builds: AnyShot
Stills and camera moves of a finished build are made by AnyShot, an AI
cinematographer that lives in its own repo and runs as its own fal app. Deploy it on the same fal account and set
fal secrets set ANYSHOT_APP=<your-fal-username>/anyshot: the builder's /scene2image, /scene2video and /shots
routes (and the optional shots order on /ref2scene) forward to it over the fal API and relay its live log, and
AnyShot reads the build straight from the account's /data storage by build_id. AnyShot also takes any uploaded
.blend, so you can call it directly.
The code both repos use (camera/, builder/blender_gpu.py, farm/app.py, pipeline/env.sh, pipeline/gpu_run.sh)
lives in AnyShot; the copies here carry a "synced from" header. Edit them there, then run
python scripts/sync_from_anyshot.py with AnyShot checked out next to this repo.
Kits: AnyKit
For props rather than whole sets, use AnyKit: give it one to four style
reference images and a Claude agent builds a cohesive kit of production-ready props in that style, at real-world
scale (per-item GLB + LOD1, a kit.blend asset library, a lineup render), in about 25-40 minutes for about $25. It
uses the same generators, agent harness and container as the scene builder, lives in its own repo and runs as its own
fal app; deployed on the same account it shares this repo's /data/anyscene/ storage and OptiX cache. AnyKit keeps
synced copies of the
// HOW IT'S BUILT
// FILES
495 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
SKILL.md18 KBf19099503a2cupstream ↗
--- name: anyscene description: Build a photoreal, film-style Blender scene from a one-paragraph brief. Generate concept images and every prop with image→3D models on fal, then script the layout, set dressing, grime, lighting, camera moves and grade. Use it when someone asks for a realistic 3D environment, a set or a cinematic shot of a place, whether starting fresh or extending an existing AnyScene scene. --- # AnyScene AnyScene turns a brief like "a 1 AM electronics repair shop in Tokyo, rain outside" into a dressed, lit Blender scene with cinematic shots and a behind-the-scenes video. Agents built the two example scenes (`scenes/tokyo-electronics-repair`, `scenes/tokyo-ramen-shop`) with this process. Each has 75–90 generated props, 8–9 shots, and about $150 of generation per round. You are the art department, set dresser, gaffer and DP. Everything is a script you can re-run; the .blend is a build output. Never hand-edit a .blend you can't regenerate. ## Requirements | Tool | Notes | |---|---| | Blender 5.0+ | `$BLENDER`. Background mode (`-b`) for everything; the GUI is only for a human to look. | | ffmpeg | `$FFMPEG`. Needs an h264 encoder (`libx264` or `h264_nvenc`), `curves`, `hue`, `vignette`, `noise`, `xfade`. | | Node 20+ | for `generate/fal.mjs` | | fal key | `FAL_KEY` env var. Never print or log it. | | GPU | 12–16 GB is enough for one Cycles render at a time. Two at once runs out of memory: see GPU rules. | ## Layout of a scene ``` scenes/<scene>/ brief.md the brief + decisions (mood, time, lens style), kept up to date (new; the reference scenes keep this in NOTES.md) assets.json room dims + every prop: id, prompt, height h, optional dims [x,y,z] picks.json per prop: chosen model + one-line why; retired / debranded flags shots.json camrig shots (see camera/README.md) NOTES.md gotchas you hit, in one line each — read it before touching the scene props/<id>.blend prepped props (output of prep) out/ renders, sheets, graded shots scripts/ the scene's build scripts: shell, dress, validate, grime, lights, post refs/<scene>/ concept images, prop references, textures/decals assets/gen/<model>/<id>/model.glb raw generator output (never edited) ``` Reusable pieces (don't fork them per scene unless you need to change them): - `generate/fal.mjs`: image and 3D generation, with an append-only spend log. - `pipeline/gpu_run.sh`: a FIFO GPU queue. - `pipeline/render_compare.py`: same-light comparison renders. - `camera/`: camera rig and shot renderer. - `breakdown/assemble.py`: the behind-the-scenes video. - `pipeline/build_scenes_page.mjs`: the gallery page. ## The pipeline Work in this order. Each step ends with a **gate**: a contact sheet or check you look at before moving on. Look at the images yourself. A script that ran without errors proves nothing about how the scene looks. ### 1. Brief → scene spec Write `brief.md`: - the place, the time of day and the weather; - 3–5 mood words; - the room size in metres; - where the street/door is (put it at y=0 and make the room deep along +Y); - zones (counter, bench, kitchen, racks); - 2–3 hero spots the camera will live on; - the light sources that would really exist there (practicals). Real places are small: the repair shop is about 3.5 × 6 m. Keep it tight. Density sells realism more than size does. ### 2. Concept images Run `node generate/fal.mjs image --variant sunburst --out refs/<scene>/concept_wide --prompt "..."`, and again for a detail shot. GPT Image 2.5 Sunburst beat Flare on legible signage text and natural grade. These set palette, clutter level and the prop list; you don't match them shot for shot. **Gate:** the user (or you, if running solo) agrees on the look. ### 3. Prop manifest List every object in `assets.json`. Sort them into four tiers: - **furniture** (scripted); - **hero props** (generated); - **mid props** (generated); - **clutter and decals** (generated or direct-modelled). Decide per object whether to generate it or direct-model it: - **Direct-model** anything architectural or simple-and-exact: walls, counters, shelving, boxes, cables, paper, glass panes, signage text (real font text objects). - **Generate** anything with character: appliances, tools, electronics, crockery, food, bins. Give every generated prop: - `h`: its real height in metres; - `dims`: for boxy things, because generators invent depth (fridges, ticket machines, ranges); - a prompt describing one object, its condition and its materials. **No brand names.** ### 4. References → 3D, both models For each prop: 1. Generate a reference with the scan template appended. This is the exact text the reference scenes used: > No real-world brand names or logos. Product reference photograph for 3D scanning. The entire object fully visible and centered, three-quarter front view from slightly above, plain seamless light-grey background, soft even diffuse studio lighting, no harsh shadows, no other objects, no hands, realistic used and worn condition, sharp focus, true-to-life materials. 2. Send the same reference to **both** Tripo H3.1 (`--model tripo-h31`) and Meshy 7.1 (`--model meshy`). Notes: - Run about 12 jobs concurrently. About 10–20% fail transiently (504, ECONNRESET, Meshy timeouts after about 10 min); re-run the same ids. The script skips GLBs that already exist. - **Never run two generation passes at once.** Existence is checked at start, so in-flight jobs get double-submitted. - References often come back with real brands even when told not to. Check every reference and regenerate those that carry one as `<id>_nb`, with a fictional brand. **Gate:** a reference sheet with no real brands. ### 5. Compare and pick Render reference | Tripo | Meshy, front, back and clay, under the same light (`render_compare.py`), then sheet them. Pick per prop and record the choice and a one-line why in `picks.json`. What the reference scenes found across ~150 props: - **Meshy 7.1** wins about 70% of the time: small consumer items, sharper printed labels. - **Tripo H3.1** wins big hard-surface heroes, where real geometry matters (knobs, bezels, dents). It's about 2M tris, so it needs decimating. - **Both** produce opaque glass, so fix glass by script. - Meshy speckles flat panels. Look at the clay render, not just the beauty render: it shows which details are real geometry. **Gate:** the picks sheet. Every prop has a winner or is retired. ### 6. Prep Turn every pick into `props/<id>.blend`: - **Front faces −Y.** Tripo's front faces +X, so rotate it −90°; Meshy's already faces −Y. Rotate the mesh data, not the object. Tripo H3.1 with `orientation: align_image` also bakes a ±20–30° Z rotation into the glTF node (to match the reference photo's three-quarter view): clear that node rotation before the −90° turn, and frame by vertex bounds, not `bound_box`. - **Real scale** from `h`/`dims`. - **Bottom-centre origin.** - **Decimate** heroes to about 150k tris. - **Textures** at 2K or less. Gotchas: - glTF imports use QUATERNION rotation mode, so `rotation_euler` is silently ignored. Set `rotation_mode = "XYZ"` first. - glTF images arrive packed. Calling `pack()` again on an unmodified image drops its data and the prop renders magenta. Repack only the images you resized. - Pass absolute paths to Blender, or relative paths resolve against the wrong directory. ### 7. Shell and layout Script the room: floor, walls, ceiling, storefront and glass, counters, shelving and fixed furniture. Name objects clearly; shots and lights reference them by name. Gotchas: - A smooth-shaded box without a bevel acts as a lens when it's glass. - `matrix_world` on objects created in the same script is stale until `view_layer.update()`. **Gate:** a clay sheet from the hero camera positions. ### 8. Dressing, big → small Place furniture props, then heroes, then mid props, then clutter. Each pass drops props onto surfaces with raycasts. - **Start drop rays below anything overhead** (shelf above, hood, conduit), or the prop lands on top of it. - **Vary instances** with `ob.color` multiplied into albedo (Object Info › Color), so repeated meshes don't read as copies. - **Run the validator after every pass.** Check each prop for: - a BVH mesh overlap against fixed furniture (lift the prop 3 mm before testing); - prop–prop clashes; - floating more than 1 cm. Start the float ray *above* the support's top face, or a thin bench top reports its own thickness. Clashing small props get spiral-searched to a clear spot. Validator reports are noisy, so read them: a plinth or pots on a burner can be legitimate. - **Re-drop after every move.** Anything a clash resolver or a later pass moves sideways must be dropped onto the surface under its new position. Skipping this left two stools hovering at table height in the dai pai dong. - **Run the grounding check** after the last dressing pass and again before final renders: `blender -b scene.blend --python pipeline/grounding.py -- --prefix <scene prefix> --max-gap 2`, then with `--all`. It casts rays down from each object's lowest vertices (feet, legs, base rings) and skips volumes. Checking from the bounding box misses floaters whenever the box bottom isn't where the object touches down. Fix floaters in the dressing scripts, not only in the saved file, so a rebuild stays grounded. Hanging items (signs, AC units, lamps, posters) are skipped unless you pass `--all`; check that each one it lists really is meant to hang. **Gate:** - The validator and the grounding check show 0 unexplained issues: no prop more than 1 cm off its support. - A dressed clay sheet and a textured flat-lit sheet. - **A contact sheet of low-angle stills** (camera ~30 cm off the floor) along the main furniture: chairs, stools, tables, bins. A floating chair is obvious from there and nearly invisible in a wide shot. ### 9. Script fixes, grime and de-branding - **Screens:** CRT screens become emissive with a scanline glow, and scopes get a graticule and trace. Fix lamp lenses, display glass and bottles to real glass. - **Grime:** a shared node group with AO-driven dirt, edge wear and per-instance tint. Add decals (stains, flyers, stickers) generated on flat backgrounds, alpha-keyed and stuck on with raycasts. - **De-branding without regenerating:** render ortho views and mark the brand rectangles. Raycast the rectangles to texels, fill each with the label's ground colour plus grain, and flatten the same texels in the normal map, or embossed text survives. ### 9b. Build the scene as a rig of dials The scene must expose the ten dials in `docs/ENDPOINT.md` (density, tidiness, wear, wetness, time_of_day, weather, atmosphere, power, warmth, look) as real controls, so they can be changed after the build without rebuilding: - **wear:** one strength input on the shared grime group, plus decal counts and label fading, all driven by one value; - **wetness:** a wetness mask feeding roughness, puddle masks and drips; - **atmosphere:** the density of every haze, dust, smoke and steam volume, scaled together; - **power:** lights and emitters in named groups (practicals, screens, fires and flames, machines), each switched or dimmed by one value; - **time_of_day, weather:** sky/world and sun setups plus weather objects (rain, mist) selected by name; - **warmth, look:** light colour temperatures and the grade preset; - **density, tidiness:** recorded as the values the dressing passes used; changing them reruns those passes. Write `dials.json` (each dial's value plus what it means in this scene) and `scripts/apply_dials.py`, which reads `dials.json` and sets every control. Apply the dials once at the end of the build, so the rig is what produced the finals. ### 10. Lighting (night) 1. Start with practicals: the light fixtures a real place would have (lamps, pendants, tubes, signage, screens, the fridge, the street spill). 2. Add fills last and keep them weak. A 45 W bounce at exposure 1.0 made the room look like the overhead lights were on. Let the practicals make the contrast. Gotchas: - **Lamp shades:** generated shades are closed meshes, so put spots just under the rim, not inside. - **Light direction:** area lights shine along local −Z. - **Glass:** hide fills from camera, glossy *and* transmission rays, or they show through glass as bright discs. - **Emissive meshes:** on dense generated meshes, texture-driven emission turns every triangle into a light. Set `material.cycles.emission_sampling = "NONE"` on those. - **Exposure:** re-check it after every new practical. **Gate:** Cycles stills of each hero spot at 32–64 spp. ### 11. Atmosphere and FX - **Volumetric haze:** cheap and worth it. - **Steam (Mantaflow):** set `surface_distance` around 1.0 (it is measured in cells), and keep emitters well inside the domain. - **Bake cache:** use negative cache frames so plumes are developed at frame 1. Clear the cache folder with `rmtree`: `fluid.free_all()` is async in background mode and deletes the new bake. - **Motion blur:** turn object motion blur **off** on volume domains, or each frame costs about 290 s. - **Rain:** particles with `rotation_mode = "VEL"` and drop geometry along +X. ### 12. Shots Write `shots.json` for `camera/shoot.py`. Its README is the contract: moves, rack focus, lens FX and the handheld profiles. 1. **Block:** start with `--framing` (3 stamped stills per shot), then `--preview` (Eevee, stamped). 2. **Design:** mix 2–3 cinematic moves (push-ins, a dolly zoom, a 100–135 mm rack) with 1–2 long **handheld walkthroughs**: `"handheld": {"profile": "walk"}`, a 4–6 key `path` at walking pace. People respond most to the walkthroughs. Keep them as long, uncut takes. 3. **Path check:** check the camera against the scene before rendering, keeping more than 0.45 m from every solid surface on every frame (walls, furniture, props: not dust motes, particles, haze volumes or decals, which a real camera moves through). Load the scene once and test every frame in one Blender session. 4. **Focus:** - A focus target must never be one the camera passes through, or the distance collapses to 0 and the frame goes soft. Rack to something past it first. - Focus `obj` uses the bounding-box centre. For deep objects, put a `pos` on the face you want sharp. **Gate:** previews reviewed. Each shot's framing, move and focus are approved before any final render. ### 13. Final renders - **GPU prefs:** `blender -b file.blend` renders Cycles on the **CPU** unless a pre-script sets the GPU compute device (`--python scenes/<scene>/scripts/gpu_prefs.py`, before any other `--python`). Device prefs are user prefs and aren't saved in the .blend. This cost the reference scenes hours. - **Chunks:** render in 24-frame chunks through `pipeline/gpu_run.sh`. Existing frames are skipped, so runs resume. - **Settings:** 2.39:1 at 1280–1920 px wide, 48–64 spp with the denoiser. That runs about 4–12 s per frame on a 5070 Ti. - **Encode:** with NVENC. Check every graded output's duration and frame count. A grade run over a frame folder with a gap silently produces a short clip. ### 14. Post Grade in ffmpeg, not the Blender compositor. The 5.0 colour-balance and mask nodes misbehave. Keep only Glare and Lens Distortion in Blender. Grade with `curves` (lift blacks to about 0.03, teal shadows, warm highlights), `hue=s=0.88`, a slight `rgbashift`, `vignette` and temporal `noise`. Deliver: - graded shots; - a reel (concat with stream copy); - a compressed copy under 30 MB for chat. ### 15. Behind the scenes (optional) `breakdown/SPEC.md` + `breakdown/assemble.py` make a one-minute video: concept, layout, props, dressing, materials, lighting, post and final shot. The scene supplies four "dollhouse" clips, one 120° orbit with the ceiling and near walls hidden: - clay layout; - props popping in; - clay to textures to grime; - lights coming on in order. ## Running several scenes or agents at once - **Ownership:** each agent owns its scene's folders and asset ids. Shared tools are read-only; copy one into your scene to change it. - **One Blender on the GPU at a time:** previews included, everything through `pipeline/gpu_run.sh`. Keep each locked call under about 10 minutes. - **Waiting on a condition:** wait on the actual condition, not a guess. A "GPU busy" check must not match its own command line, and killing a waiting shell does not kill its child processes. Both of these caused duplicate renders that overwrote finished work. After a pause or crash, list every `blender` process before starting anything. - **Cross-scene ordering:** marker files only, e.g. `.gpu.<scene>_done`, and create one only when the work is actually done. - **Other GPU apps:** another GPU-heavy app (Houdini, Unreal) running alongside can slow Cycles 8×. Tell the user rather than closing their apps. ## Budget and time (fal, Oct 2026) | Item | Cost | |---|---| | Reference image | ~$0.25 | | Tripo H3.1 | ~$0.60 | | Meshy 7.1 | ~$0.80 | | Per prop, both models | **~$1.65** | | ~90-prop scene | ~$150 | Every call is logged to `generate/spend.jsonl`; `node generate/fal.mjs spend` totals it. Time on one 16 GB GPU: a scene is roughly a day of agent work, and the final shots are about 1–2 GPU-hours. ## Generalising to a new scene 1. Start from the closer example scene: copy its `assets.json`, `shots.json` and `scripts/` into `scenes/<your-scene>/` (name the folder after the place, e.g. `lisbon-tram-depot`), then rewrite `brief.md` and `assets.json` for it. 2. Reuse the shared tools as they are. Reuse an example scene's `scripts/` as a starting point, but the shell and the dressing passes are always scene-specific. That's where your judgement goes. 3. Keep `NOTES.md` as you go. Anything that cost you more than 10 minutes gets one line. The next agent reads it first. ## Definition of done - [ ] Every placed prop is in `picks.json` with a reason, and no real brands are visible in any shot. - [ ] The validator is clean, or every remaining report is explained. - [ ] `pipeline/grounding.py` reports 0 unintended floaters on the file the finals are rendered from. - [ ] Each hero spot has a final still, and every shot is rendered, graded and has the right duration. - [ ] There's a reel and a compressed copy; an optional BTS video. - [ ] `NOTES.md` is updated, and the spend is totalled.blender_addon/README.md10.0 KB55d82c7e9569upstream ↗
# fal Render Farm (Blender add-on) Render the open scene on the AnyScene render farm (fal Serverless, RTX PRO 6000 GPUs) instead of your local GPU. Direct a downloaded scene in your own Blender (cameras, lights, edits), then press **Render animation on fal**. Frames come back into the scene's own output path with the same names a local render would give them. ``` blender_addon/ fal_render_farm/ the add-on (an extension folder: blender_manifest.toml + bl_info, works either way) __init__.py panel, operators, preferences, status bar, UI timer job.py planning (main thread) and the job runner (background thread) fal_api.py fal queue API + fal storage upload over plain urllib pack_copy.py runs in a separate Blender on a temp copy: make local, pack_all, refuse on externals farm_gpu.py shipped in every bundle, run with --python on the farm: OptiX/CUDA + GPU on every scene fal_render_farm-1.0.0.zip installable build tests/ make_test_scene.py, test_farm.py (headless end-to-end test against the real farm) ``` ## Install Blender 4.2 or newer (built and tested on 5.0.1). 1. **Edit > Preferences > Get Extensions**, then the drop-down at the top right > **Install from Disk...**, and pick `blender_addon/fal_render_farm-1.0.0.zip`. Dragging the zip onto the Blender window also works. 2. Enable **fal Render Farm** if it isn't enabled already. For development, symlink or copy `blender_addon/fal_render_farm/` into an extensions repo (or a legacy `scripts/addons/` folder) instead. ## Set the key Open the add-on's preferences (Preferences > Add-ons > fal Render Farm): - **fal key**: the key of the account that owns the farm app (the Serverless key the farm is deployed under, `FAL_SERVERLESS_KEY` in this repo, not a generation-only `FAL_KEY`). It's a password field, stored in Blender's user preferences (`userpref.blend`), and the add-on never prints or logs it. Press **Check key** to make sure fal accepts it. - **Farm app**: empty by default; set it to your own deployment of `farm/app.py`, e.g. `<your-fal-username>/blender-farm-2` (see "Farm change" below). The add-on refuses to render until it's set. A farm deployed from the older build as `blender-farm` returns **PNG frames only**; `blender-farm-2` is the same farm with one backward-compatible addition (an `outputs` field), so it also returns EXR, multilayer EXR, JPEG, TIFF and so on. The panel warns when the output format isn't PNG and the app is a `.../blender-farm`. - **Frames per chunk** (24) and **Concurrent chunks** (20): one chunk is one farm request on one GPU. - **Chunk timeout**, **Encode MP4 when done**, and the **ffmpeg** path (empty means use the one on PATH). Remember to save preferences (or keep auto-save on). ## Render **Properties > Render > fal Render Farm**: - **Render animation on fal** renders the scene's frame range (and frame step). - **Render this frame on fal** renders the current frame and saves it to the output path as frame N. If an Image Editor is open, the frame is shown there when it arrives. - **Cancel** cancels in-flight chunks on fal. Frames that already arrived stay on disk. - **Retry failed chunks** re-submits failed chunks against the bundle that's already uploaded. What happens when you press render: 1. The open file, unsaved edits included, is written to a temp folder with `save_as_mainfile(copy=True)`. Your working file isn't modified or re-saved, and it doesn't get marked dirty. 2. A separate background Blender opens that copy, makes linked data local, runs `pack_all` and saves `scene.blend`. If something still points outside the file (unpackable images or movies, Alembic/USD caches, libraries), the render stops and the panel lists those paths. 3. `scene.blend` and `farm_gpu.py` are zipped, the `bundle_id` is set to the first 16 hex characters of the zip's sha256, and the zip is uploaded to fal storage. An unchanged bundle is uploaded only once per session. 4. Frames that already exist at their output path are **skipped** (that's how resume works). The remaining frames are cut into chunks. Each chunk goes to the queue API as `{bundle}/scene.blend -S <scene> --python {bundle}/farm_gpu.py -o {out}/frames/<filename pattern> -s A -e B -j STEP -a`, and at most *Concurrent chunks* are in flight at once. 5. Each finished chunk's zip is downloaded and its files are written to the paths that `scene.render.frame_path()` gives locally. That covers `#` padding, relative `//` paths and the file extension setting. The scene renders with its own settings: engine, samples, resolution and percentage, file format and colour depth, view layers, compositor, color management and camera. The only thing the farm changes is the device: `farm_gpu.py` selects OptiX (or CUDA) and sets `cycles.device = 'GPU'`, the same way `builder/blender_gpu.py` does for the builder. Progress appears in the panel (phase, chunks queued/rendering/done/failed, frames, elapsed time, GPU-seconds and their cost) and in the status bar. Everything runs on a background thread, and a timer redraws the UI every 0.5 s, so Blender stays responsive. Each failed chunk shows its error. A chunk is retried once automatically before it's marked failed. When an animation finishes with PNG/JPEG/TIFF/... frames and ffmpeg is available, the add-on writes an H.264 MP4 (`<name>_<first>-<last>.mp4`, CRF 16) next to the frames. EXR frames are skipped, because they're linear and belong in your grade. ## What the estimate means The estimate scales a measured reference to your scene. Mission control rendered at **5 to 7 s per frame at 1920x804 and 48 spp** on an RTX PRO 6000. That figure is multiplied by `(your pixels / 1920x804) x (your samples / 48)`, using Cycles samples or EEVEE render samples. Each chunk also adds about 20 s of overhead (Blender start, scene load, returning the frames). GPU time is priced at **$4.00 per GPU-hour**. Wall time assumes chunks run *Concurrent chunks* at a time, plus about 90 s of cold start if the app has scaled to zero. Treat the estimate as an order-of-magnitude figure, not a quote. Scene complexity, volumes, adaptive sampling, denoising, light paths and EEVEE versus Cycles can each move the real number by 2x or more. Once a job is running, the panel shows the real GPU-seconds the farm reports (render time plus bundle fetch per chunk). That figure leaves out cold starts and idle keep-alive, which fal also bills. ## Troubleshooting - **"Set your fal key"** or **HTTP 401/403**: the key is missing, or it belongs to an account that can't see the farm app. Use the farm owner's Serverless key and press **Check key**. - **"the file references data that can't be packed"**: these are the listed images/movies/caches. Fix their paths or make them local. Fluid and other disk simulation caches aren't shipped (you get a warning, not an error), so bake them into the mesh or render those shots locally. - **"output path is relative (//) but this file has never been saved"**: save the file once, or set an absolute output path. - **"Output is a video format"**: the farm renders frame sequences in chunks. Pick PNG/EXR/... and turn on *Encode MP4*. - **A chunk fails with "the farm returned no OPEN_EXR files"**: you're on a `blender-farm` deployment, which only returns PNG. Deploy and switch to `<your-fal-username>/blender-farm-2`. - **Chunks sit in "queued"**: the farm is busy or scaling up (it allows at most 20 workers, shared with other jobs on the same app). They start once workers free up. - **A chunk timed out**: lower *Frames per chunk*, or raise *Chunk timeout* (the farm's hard cap is 3600 s per request). - **Frames look different from a local render**: the farm runs Blender **5.0.1** on Linux. Scenes that depend on add-ons (custom nodes, drivers that call add-on Python, Python-scripted drivers in general) only render correctly if the farm has them, and it has no add-ons. Disk caches and file-output nodes with absolute paths aren't shipped either. - **Nothing comes back for a stereo/multiview scene**: per-view files (`_L`/`_R`) are written next to the frames, but resume doesn't know about them yet. - The **System Console** (Window > Toggle System Console) shows one `fal render farm:` line per step and per chunk. ## Tests ``` "C:/Program Files/Blender Foundation/Blender 5.0/blender.exe" -b --factory-startup --python-exit-code 1 \ --python blender_addon/tests/test_farm.py -- <workdir> --app <your-fal-username>/blender-farm-2 [--chunk 12] [--ffmpeg <path>] BLENDER_USER_RESOURCES=<empty temp dir> "C:/Program Files/Blender Foundation/Blender 5.0/blender.exe" -b \ --factory-startup --python-exit-code 1 --python blender_addon/tests/test_zip_install.py -- blender_addon/fal_render_farm-1.0.0.zip ``` Rebuild the zip with `blender --factory-startup --command extension build --source-dir blender_addon/fal_render_farm --output-dir blender_addon`. The test builds a 48-frame 640x360 Cycles scene with an external texture and enables the add-on from its folder. It reads `FAL_SERVERLESS_KEY` from the Windows user environment, for that process only, then draws the panel and polls the operators. It renders the animation through the operator and checks every frame name. It also confirms the working .blend is untouched, tests resume (it deletes 2 frames and re-renders), compares frame 1 against a local GPU render (PSNR), and renders one EXR frame. It ends with a `TEST-SUMMARY` JSON line. ## Farm change (blender-farm-2) `farm/app.py` gained one optional request field, `outputs` (glob patterns relative to `{out}`). It defaults to `["**/*.png"]`, which is exactly the old behaviour, so existing clients (`farm/farm.py`, AnyShot's `farm_render`) are unaffected. The add-on always sends `["frames/**/*"]`, and an older `blender-farm` deployment ignores the unknown field. The changed app is deployed as `blender-farm-2` on your own account: ``` fal deploy farm/app.py::BlenderFarm --app-name blender-farm-2 --auth private ``` An existing `blender-farm` deployment doesn't need to be redeployed.camera/README.md13 KB29f39c6c1354upstream ↗
<!-- synced from github.com/apekshik/anyshot camera/README.md --> # camrig: cinematic camera rig and shot renderer `camera/shoot.py` adds one camera rig per shot to a scene .blend, then renders: - preview blocking passes, - framing checks, - lens charts, - final Cycles frames and mp4s. Shots are described in a JSON file: an AnyShot order writes its own as `out/shots/<job>/shots.json`, and an AnyScene build keeps its scouting cameras in `out/shots.json`. Examples live in `camera/examples/`. The contract is stable. New fields may be added, but existing ones will not change meaning. ## CLI ```bash . pipeline/env.sh # sets $B (Blender) and $ROOT_ABS (absolute repo root) # always through the shared GPU queue pipeline/gpu_run.sh "$B" -b scenes/my-set/set.blend --python camera/shoot.py -- \ --shots $ROOT_ABS/scenes/my-set/shots.json \ --out $ROOT_ABS/scenes/my-set/out/shots \ [--only a,b] [--preview | --framing | --lenschart NAME] [--frames 1-24] [--still 12] [--save rigged.blend] [--no-encode] \ [--engine eevee|cycles|workbench] [--step N] ``` | Mode | What it does | Output (`<out>/<shot>/…`) | |---|---|---| | *(default)* final | Renders with Cycles. Scene settings stay unless a shot field overrides them. Frames that already exist are skipped, so a run can be resumed. Encodes `<shot>.mp4` and a contact sheet once every frame exists. | `frames/####.png`, `<shot>.mp4`, `contact.png` | | `--preview` | Renders with Eevee at 50% resolution, every frame (`--step N` renders every Nth), 16 TAA samples, no motion blur. The stamp is burned in per frame: shot name, frame, lens mm, f-stop, focus distance. `--engine cycles` gives a 16 spp denoised preview with the real lighting. | `preview/####.png`, `preview.mp4` | | `--framing` | Renders a stamped low-res still of each shot at t = 0, 0.5 and 1 with Eevee, plus one 3×3 sheet for all shots. | `framing/t0.png`, `framing/t5.png`, `framing/t10.png`, `<out>/_framing.png` | | `--lenschart NAME` | Renders shot NAME's t=0 position and aim at 18/24/35/50/85/135 mm with Eevee. | `lenschart/<mm>.png`, `<out>/<NAME>_lenschart.png` | | `--frames a-b` | Renders only frames a..b of the shot (1-based). Use it to keep each `gpu_run` call short (aim for under about 10 minutes). | | | `--still N` | Renders a single final-quality frame N. | `still_####.png` | | `--save path.blend` | Writes the rigged scene so a human can open and tweak it in the UI. It never overwrites the input file. | | | `--no-encode` | Skips mp4 and contact-sheet encoding. | | The script never saves the input .blend. Every shot's rig is built fresh on each run. ## shots.json ```jsonc { "defaults": { "fps": 24, "res": [1920, 1080], "aspect": "16:9", "samples": 256, "shutter": 180, "sensor": "s35", "fstop": 2.8, "ease": "inout", "handheld": 0.0 }, "shots": [ { "name": "counter_push", // [a-z0-9_], used for folders and the rig name "frames": 120, // length in frames "fps": 24, "res": [1920, 1080], "aspect": "2.39", // "16:9" | "1.85" | "2.39" — output is cropped to the aspect (res height recomputed from width) "samples": 256, // Cycles samples for final "shutter": 180, // motion blur shutter in degrees (0 = off) "sensor": "s35", // "s35" (24.89 mm) | "ff" (36 mm) | number in mm (sensor width) "move": "push_in", // see Moves "ease": "inout", // linear | in | out | inout — position/aim: one ease over the whole shot (passes through middle keys without stopping); lens/roll/focus/f-stop keys: eased per segment (hold + pull) "keys": [ { "t": 0.0, "pos": [x,y,z], "aim": [x,y,z], "lens": 35, "roll": 0 }, { "t": 1.0, "pos": [x,y,z], "aim_obj": "ramen_bowl_unfinished", "lens": 35 } ], "focus": { "mode": "rack", "keys": [ { "t": 0.0, "obj": "water_glass" }, { "t": 0.6, "pos": [x,y,z] } ] }, "fstop": 2.0, // or "fstop_keys": [{ "t":0, "v":4 }, { "t":1, "v":1.4 }] "blades": 0, "ratio": 1.0, // aperture blades (0 = round) and anamorphic ratio (>1 = oval bokeh) "handheld": 0.35, // 0..1, shake layered on top of the move "breathing": 0.5, // 0..1, slow drift (default = handheld) "lens_fx": { "distortion": 0.01, "dispersion": 0.01, "glare": "fog" } // optional, see notes } ] } ``` Units are metres and Blender world axes (Z up). Wherever the contract takes a position, an object name works too. - `aim_obj` and `focus.obj` resolve to the **centre of that object's world bounding box**. - `obj` matches the object name exactly. If nothing matches exactly, it uses the first object whose name starts with the given string, then the first case-insensitive substring match. Bounds include all mesh children, so a GLB import root works. - `"aim_obj": "name", "aim_offset": [dx,dy,dz]` nudges the aim point from that centre. Keys also take `pos_obj` + `pos_offset`. A key that omits `pos` or `aim` holds the previous one. - Optional `"exposure": EV` offsets the scene exposure for the shot. The scenes' old keyed exposure drift is removed and frozen at its frame-1 value, so it can't leak into new shots. ### Moves All moves interpolate between `keys`; `move` sets defaults and validation: | move | keys needed | notes | |---|---|---| | `static` | 1 | Locked off. Add `handheld` for life. | | `dolly`, `truck`, `pedestal`, `push_in`, `crane` | 2 or more | Each key has a position and an aim. The names describe intent; they all interpolate the same way. | | `pan`, `tilt` | 2 or more | Keep `pos` the same and change `aim`. | | `orbit` | none (or 1 for lens and roll) | `"orbit": {"center":[x,y,z] or "center_obj":"name", "radius":1.2, "start_deg":-30, "end_deg":30, "height":1.3}`. Aims at the centre. Angles are measured from +X, counter-clockwise from above. | | `dolly_zoom` | 2 (start and end `pos`; aim and lens at t=0) | Lens is solved per frame to hold the subject size: `lens(t) = lens0 · dist(t) / dist0`. Distance is measured to `"subject"` (pos or obj, defaults to the aim). | | `path` | 3 or more | Smooth path through every key: Catmull-Rom on position, eased in time. | ### Focus - `{"mode":"track","obj":"name"}`: the focus empty is parented to and follows that object's bounds centre. - `{"mode":"point","pos":[x,y,z]}` - `{"mode":"rack","keys":[{"t":0,"obj":"a"},{"t":0.6,"pos":[...]}]}`: the focus empty moves between points with the same ease. The pull happens between successive keys, so to hold and then pull, repeat a key: `t 0 → a`, `t 0.4 → a`, `t 0.6 → b`. - `{"mode":"none"}`: depth of field off. - `fstop` (or `fstop_keys`), `blades` and `ratio` are optional. ### Handheld - **Shake (`handheld`):** two Noise layers per channel on `CR_<shot>_shake` (between aim and camera, so it moves in camera space). - Hand bob: period about 11 frames, 4 mm and 0.27° × intensity. - Fine jitter: period about 3.5 frames, 1.2 mm and 0.07° × intensity. - Roll runs at about 60% of pan and tilt. - **Breathing:** body sway with a period of about 2 s, 14 mm and 0.4° × breathing. - **Lens length:** the shift scales with focal length the way real handheld does. At 85 mm, handheld 0.4 already reads clearly; at 24 mm, 0.3–0.5 feels like a steady operator. ### Handheld walk profile (opt-in) `"handheld": {"profile": "walk", "amount": 1.0}` replaces the noise shake with a baked operator-walking motion. Shots that keep a number for `handheld` are unchanged. - **Footsteps:** a bob of about 1 cm, once per step at 1.8 Hz (`step_hz`), and a 0.3° roll that alternates left and right with each step. Both scale with the speed of the move's path: full at or above `walk_speed` (default 0.55 m/s), zero when standing. Walking intensity is smoothed over about 0.4 s, so steps fade in and out with the move's ease. - **Body sway:** 0.17–0.57 Hz. Position moves ±2–3 cm and pan/tilt ±0.7°. When the operator stands still this drops to about 45%. - **Aim lag:** the aim follows a spring (`aim_lag_hz` 0.9, `aim_damping` 0.62). Pans trail behind the keyed aim and overshoot slightly when they settle. Focus that follows the aim (the default focus mode) uses the lagged aim as well. - **Reframes:** `corrections` (default 2) small reframes. Each drifts off by 0.6° and is quickly corrected back. - **Frequency ceiling:** nothing is above about 1.9 Hz, so there is no jitter. Motion blur sees smooth curves. - **Baking:** everything is baked per frame onto `CR_<shot>_shake` (location in metres, rotation in radians, in the camera's local axes). It is deterministic per shot index. - **Setting it up:** use `move: "path"` with 4–6 keys at a walking pace (about 0.3–0.6 m/s) and `ease: "inout"`, so the operator starts and stops gently. Check the camera path against the scene before rendering: load the scene once, build a BVH of its meshes, and report the closest mesh and its frame for every frame of the move. ## Rig hierarchy (in the saved .blend) ``` CR_<shot>_base (location baked one LINEAR key per frame) └ CR_<shot>_aim (rotation: Track To an animated aim empty CR_<shot>_target) └ CR_<shot>_shake (noise only) └ CR_<shot>_cam (camera data: lens/sensor/DOF; lens animated) CR_<shot>_target (aim point, animated) CR_<shot>_focus (DOF focus object, animated or parented) ``` Moves are evaluated in Python and **baked one key per frame**, so every move type behaves the same and Cycles motion blur sees smooth subframes. The real way to change a move is to edit shots.json and rerun. In a `--save`d file you can still: - turn the shake empty's Noise modifiers up or down (Graph Editor → Modifiers), - move the focus empty, - change lens and f-stop keys on the camera data. Each shot's rig lives in its own collection `camrig/CamRig_<shot>`. Rerunning a shot deletes and rebuilds that rig. ## Lens FX / grading (Blender 5.0) - `lens_fx.distortion` and `lens_fx.dispersion` use the **Lens Distortion** compositor node. `lens_fx.glare` (`"fog"`, `"bloom"`, `"streaks"`) uses the **Glare** node. - **Vignette, colour balance and grain are not done here.** The 5.0 mask and colour-balance nodes misbehave, so finish those in your own ffmpeg grade pass on the rendered frames. - **Existing compositor:** if the scene already has one, it is kept unless the shot sets `lens_fx`. In that case camrig copies the scene's tree to `camrig_fx` and splices its nodes in just before the Group Output. (Wrapping the old tree in a group node renders **black** in 5.0, because Render Layers inside a nested group yields nothing.) ## Encoding - ffmpeg comes from `$FFMPEG` (default `ffmpeg` on PATH). camrig encodes with `h264_nvenc` (CQ 18, yuv420p, faststart), which is fast and barely touches the GPU. - If NVENC fails, it falls back to `h264_mf`. - Frames are fed through an ffmpeg concat list, so no glob is needed and stepped previews work. 16-bit PNG frames encode fine. ## Helpers (python, inside Blender) ```python import sys; sys.path.append("<repo>/camera") import camrig camrig.bounds("ticket_machine") # -> (min Vector, max Vector, center Vector) camrig.find("ramen_bowl") # exact name or prefix match ``` `--list "pattern"` prints the matching objects with their bounds centre and size, then exits: ```bash "$B" -b scene.blend --python camera/shoot.py -- --list bowl ``` ## Measured (RTX 5070 Ti, Blender 5.0.1, on two of AnyScene's reference sets) | Pass | Scene | Time | |---|---|---| | `--framing`, 4 shots → 12 Eevee stills at 640 px | either | 30–50 s, including the file load | | `--preview`, 960×402 Eevee | ramen | ~0.25 s/frame | | `--preview`, 960×402 Eevee | repair | ~0.35 s/frame | | `--lenschart` | either | ~12 s | | Final Cycles, 960×402, 32 spp, motion blur, glare + lens distortion | repair | ~10 s/frame | | Final Cycles, 640×360, 16 spp | repair | ~3.3 s/frame | Scale final times by pixels × samples before you pick a `--frames` chunk size. ## Robust vs flaky (Blender 5.0) **Robust:** - every move type, including dolly_zoom lens solving and Catmull-Rom paths - Track To aim - rack, track and point focus - f-stop keys and blades/ratio - Noise-modifier handheld - per-frame stamps (a `frame_change_post` handler updates the note) - Eevee previews - resume via `use_overwrite=False` - NVENC encode - Glare and Lens Distortion spliced into the scene's compositor **Avoid:** - compositor mask, ellipse and colour-balance nodes (grade in ffmpeg) - nesting the scene's compositor group (renders black) - Track To flips when the aim is exactly vertical, so keep aim points off the camera's plumb line **Eevee previews differ from Cycles:** - Glass reads as dark. - Light falloff is flatter. - Previews are for framing, move and focus, not for look. ## Examples - `camera/examples/ramen_shots.json` and `camera/examples/repair_shots.json`: 35 mm push-in with a foreground->subject rack, 85 mm static handheld at f/1.8, 24 mm orbit, and a dolly zoom, written for two of AnyScene's reference sets (the object names are from those scenes).README.md8.8 KBf4bec83b346aupstream ↗
# AnyScene Give it reference images of a place (or a one-paragraph brief) and it builds a dressed, lit Blender scene: a real room with every prop as its own model, practical lights, grime, haze, cinematic camera moves and graded renders. The work is done by an AI agent (Claude, in Claude Code) following [`SKILL.md`](SKILL.md). No prop is modelled by hand: each one is generated as an image, turned into 3D with two image-to-3D models on [fal](https://fal.ai), and the better result is picked by comparing same-light renders.  ## Five places, one process | | Scene | | |---|---|---| |  | **Electronics repair shop**, Tokyo, 1 AM, rain outside | [`scenes/tokyo-electronics-repair`](scenes/tokyo-electronics-repair) · 86 generated props | |  | **Ramen shop**, Tokyo, last orders, steam and rain | [`scenes/tokyo-ramen-shop`](scenes/tokyo-ramen-shop) · 74 generated props | |  | **Dai pai dong**, Hong Kong, an open-air street kitchen just after rain | [`scenes/hk-dai-pai-dong`](scenes/hk-dai-pai-dong) · 78 generated props | |  | **Mission control**, 1970s, a night shift during an orbital flight | [`scenes/mission-control-1970s`](scenes/mission-control-1970s) | |  | **Maison Roussel**, a Paris bakery. Built by the hosted endpoint from [one reference image](docs/media/bakery_reference.jpg), with no human input, in under two hours | (endpoint run) | The first four were built by an agent working through `SKILL.md`, with a person checking its gates. The bakery was built end to end by the same process running unattended on a fal GPU worker. This repo is the process and the code. The finished scenes (.blend files, generated models, textures, renders) add up to tens of gigabytes and are not in git. Each scene folder keeps what's needed to rebuild it: its prop list (`assets.json`), model picks with reasons (`picks.json`), shots (`shots.json`), notes and build scripts. ## Quick start You need: - [Blender](https://www.blender.org/) 5.0 or newer. Set `BLENDER` if it isn't on your PATH or in the default install location. - [Node](https://nodejs.org/) 20 or newer, then `npm install` in the repo root. - A [fal API key](https://fal.ai/dashboard/keys) in `FAL_KEY`. Generation is billed to your fal account: roughly $1.65 per prop (an image plus two 3D models), so about $150 for a 90-prop scene. - ffmpeg with an H.264 encoder (`FFMPEG`, default `ffmpeg`). The example scenes were built on Windows with an NVIDIA GPU and use `h264_nvenc`; on other machines, swap in `libx264`. - [uv](https://docs.astral.sh/uv/) for the Python helper scripts. - [Claude Code](https://claude.com/claude-code). - A GPU with 12–16 GB of memory for Cycles. Then open the repo in Claude Code and ask for a scene: > Use SKILL.md to build a new scene: a Lisbon tram depot at dawn, one tram half-stripped for repair. Or start from your own references: put a few images (and, optionally, a floor plan) in `refs/<scene>/` and say so: > Use SKILL.md to build a scene from the reference images in refs/lisbon-tram-depot/. Treat them as the approved concept. Check the agent's work at each gate: concept, prop picks, clay layout, lit stills, shot previews. Expect most of a day of agent time and 1–2 GPU-hours of final renders per scene. ## How the agent works [`SKILL.md`](SKILL.md) is the whole process, step by step, with every gotcha the example scenes hit: 1. brief → scene spec (room size, zones, hero spots, real light sources); 2. concept images, or your references; 3. a prop manifest, then a reference image for every prop; 4. each prop through both Tripo H3.1 and Meshy 7.1, picked by looking at same-light renders; 5. a scripted room shell, dressing from big furniture down to small clutter, with collision and floating checks; 6. grime, de-branding, decals, practical lighting, haze, steam and rain; 7. cinematic and long handheld walkthrough shots, rendered in Cycles and graded; 8. optionally, a behind-the-scenes video. Every step ends with a gate: a contact sheet the agent has to look at before moving on. Everything is a script, so the .blend is a build output that can be regenerated. [`builder/CRAFT.md`](builder/CRAFT.md) is the short version of what made the good scenes good: render it and look, name the problem precisely, fix it in code, check the fix from a view that shows it, and get an independent critic. ## Layout ``` SKILL.md the process; agents start here builder/CRAFT.md the review loop that separates good scenes from first drafts generate/ fal client: images and image→3D (Tripo H3.1, Meshy 7.1), with a local spend log pipeline/ Blender/ffmpeg discovery, a FIFO GPU queue, comparison renders, gallery pages, floating-prop check camera/ camera rig and shot renderer: moves, rack focus, handheld walk, lens effects (synced from AnyShot) breakdown/ behind-the-scenes videos, promo cuts and the launch supercut scenes/<name>/ one folder per scene: assets.json, picks.json, shots.json, NOTES.md, brief.md, scripts/ farm/ a fal Serverless render farm for Cycles (app.py, synced from AnyShot), and a client that renders a scene's shots on it blender_addon/ the fal Render Farm add-on for Blender builder/ the same process packaged as a fal Serverless app (the hosted endpoint) docs/ endpoint and API notes, README images scripts/ sync_from_anyshot.py: refresh the files shared with AnyShot from a sibling ../anyshot checkout ``` ## Hosted endpoint (optional) If you'd rather not set up Blender, a GPU and the toolchain, the hosted endpoint runs the whole build on fal: send one to four reference images (and an optional prompt), and get back the scene package (`scene.blend` with every prop as its own object, a `scene.glb`, stills and panoramas; walkthroughs render on the farm). The bakery above was built this way, from a single image. <!-- TODO(apek): link the public endpoint page / model id here once it's listed. --> The endpoint's code is in [`builder/`](builder) if you want to deploy your own copy to your fal account (`fal deploy builder/app.py::SceneBuilderApp --app-name anyscene-builder`). It needs `FAL_KEY` and `OPENROUTER_API_KEY` set as secrets on the app, and the `endpoint/` folder alongside it (the builder imports its planner and schema). ## Shooting builds: AnyShot Stills and camera moves of a finished build are made by [AnyShot](https://github.com/apekshik/anyshot), an AI cinematographer that lives in its own repo and runs as its own fal app. Deploy it on the same fal account and set `fal secrets set ANYSHOT_APP=<your-fal-username>/anyshot`: the builder's `/scene2image`, `/scene2video` and `/shots` routes (and the optional `shots` order on `/ref2scene`) forward to it over the fal API and relay its live log, and AnyShot reads the build straight from the account's `/data` storage by `build_id`. AnyShot also takes any uploaded `.blend`, so you can call it directly. The code both repos use (`camera/`, `builder/blender_gpu.py`, `farm/app.py`, `pipeline/env.sh`, `pipeline/gpu_run.sh`) lives in AnyShot; the copies here carry a "synced from" header. Edit them there, then run `python scripts/sync_from_anyshot.py` with AnyShot checked out next to this repo. ## Kits: AnyKit For props rather than whole sets, use [AnyKit](https://github.com/apekshik/anykit): give it one to four style reference images and a Claude agent builds a cohesive kit of production-ready props in that style, at real-world scale (per-item GLB + LOD1, a `kit.blend` asset library, a lineup render), in about 25-40 minutes for about $25. It uses the same generators, agent harness and container as the scene builder, lives in its own repo and runs as its own fal app; deployed on the same account it shares this repo's `/data/anyscene/` storage and OptiX cache. AnyKit keeps synced copies of the code it shares with this repo (`builder/gallery.py`, `builder/blender_gpu.py`, `generate/fal.mjs`, `pipeline/env.sh`, `pipeline/gpu_run.sh`), so edit those here first. The web UI's kit gallery calls it over the fal API (`NEXT_PUBLIC_ANYKIT_APP`). ## Render farm add-on [`blender_addon/`](blender_addon) is a Blender add-on that renders the open scene on fal GPUs instead of your own: direct a scene in Blender, press **Render animation on fal**, and the frames come back into the scene's output path with the same names a local render would give them. See [`blender_addon/README.md`](blender_addon/README.md) for install and settings. The farm itself is [`farm/app.py`](farm/app.py) (its home is AnyShot's `farm/app.py`); you can deploy your own and point the add-on's **Farm app** preference at it. ## Licence Code is MIT, see [LICENSE](LICENSE). Generated assets are not in this repo; where they're published, they come with their own licence.unreal_fal/README.md5.6 KB60c4e75f1004upstream ↗
# unreal_fal: the Blender -> Unreal export stage on fal Serverless `app.py` is the fal app **`apek/blend2unreal`** (private). It runs `export_blend.py` headless on RTX 5090 workers. The slow texture bake is sharded across workers, and finished assets are cached by content hash on `/data`. ```bash cd unreal_fal fal deploy app.py::Blend2Unreal --app-name blend2unreal --auth private --reset-scale python client.py upload hk_scene.zip # .blend or .zip (a .blend + grade.sh); pack images/fonts first python client.py export <scene_url> --options '{"frame": 60, "dof": {"cam_street": [9.5, 2.8]}}' --out result.json ``` ## API `POST /export` ```json {"scene_url": "https://.../hk_scene.zip", "options": {"frame": 60, "density": 1024, "max_res": 4096, "dof": {"cam_street": [9.5, 2.8]}, "grade_sh": "grade.sh", "blend": null, "device": "gpu", "shards": 0, "procs": 0}, "use_cache": true, "fresh": false, "inline_scene_json": true} ``` Response: ```json {"job_id": "x_...", "zip_url": "...zip", "scene_json_url": "...scene.json", "scene_json": {...}, "report": {"counts": {...}, "cache": {...}, "not_baked": [], "not_converted": [...], "warnings": [...], "timings": {...}, "worker_seconds": {...}, "shards": [...]}, "minutes": 5.4} ``` - `device`, `shards` and `procs` don't change the result, so they are left out of the retry fingerprint. - The app scales to zero (`min_concurrency = 0`), so a cold `/export` first waits ~1-2 min for a worker to boot. Set it to 1 to keep a warm worker ($2.50/h). - Repeating an identical request returns the finished result, or attaches to a live attempt (heartbeat), or resumes a dead one and re-attaches to its shard requests. `fresh: true` exports again. - `use_cache: false` re-bakes everything into a job-private cache, deleted afterwards. The shared cache is never read or changed. `POST /shard` is internal: `/export` calls it on other workers. Benches call it with `use_cache: false`. ## Pipeline 1. **Plan** (`--plan-out`, ~25 s for HK): names every unique mesh, hashes it and estimates its bake cost. - The hash covers: evaluated mesh, material node trees (recursively, with packed image bytes), world-space scale, bake settings, the Blender version and the exporter file itself. - Materials that read their object (Object Info, object coordinates, object attributes) also hash that object. 2. **Shards**: - Cache misses are dealt longest-first into N shards: ~2 min of estimated work per Blender process, at most 32. - Shard 0 runs on the `/export` worker; the others are `/shard` requests, submitted and polled (not `subscribe()`). - Each worker runs 2 Blender processes x 3 threads with OptiX. Each process is resumed up to 6 times after a Blender bake crash. - Every finished asset is written to `/data/blend2unreal/cache/<h[:2]>/<h>/` (written to a temp dir, then renamed into place). - Assets still missing after round 1 are re-dealt in a second round. 3. **Merge** (`--plan --cache --link-cache --no-bake`): pulls every asset from the cache by symlink, then writes volumes, lights, cameras, world, grade LUT and `scene.json`. 4. **Package**: copies off `/data` with 16 parallel readers, zips (PNG/EXR stored, the rest deflated) and uploads to fal storage. ## Machine notes (measured) - An RTX 5090 worker is **6 cores / 60 GB** (`os.cpu_count()` says 512: the host's). Probed: - L: 4 cores / 15 GB - XL: 8 / 30 GB - RTX PRO 6000: 6 - H100: 12 / 112 GB - Per-asset time is mostly CPU: unwrap, Cycles sync, PNG encode. - 36-asset HK bench on one worker: | Setup | Wall time | |---|---| | GPU, 2 procs x 3 threads | 122 s | | GPU, 1 proc x 6 threads | 196 s | | CPU, 2 procs | 785 s (road alone 354 s) | **GPU (OptiX) is ~6x faster**: it is the default. - OptiX compiles its "shader raytrace" kernels (AO and Bevel nodes) on the first such bake: ~230 s per process on a fresh worker. The warm-up in `setup()` now bakes an AO+Bevel material, and the kernel cache is shared on `/data` with the render farm (`/data/anyscene-farm/optix/<gpu>`). ## Changes to merge back into `unreal/` `export_blend.py` (copied from `unreal/` at 21:15 on Nov 1, after its last edit at 20:25): 1. New flags: `--plan-out FILE`, `--plan FILE`, `--keys FILE`, `--cache DIR`, `--link-cache` (see the docstring). Without them the exporter behaves as before, apart from items 2-4. 2. **Asset names are computed over the whole sorted key list before any filtering by `--keys`.** - Without `--only`/`--limit` the names are the same as before. - With them, the collision suffix `_NNN` now uses the index in the full list. Before, it was the index in the filtered list. 3. `scene.json` assets gain `"hash"` when `--cache` is used. Shard mode writes `out/shard_<keys file>`, not `scene.json`. 4. With `--gpu`, the bake scene selects OptiX devices itself (a factory-startup Blender would otherwise bake on the CPU). Locally, `gpu_prefs.py` via `EXPORT_PRE` keeps working. `build_level.py`, `render_shots.py`, `side_by_side.py`, `agx_view.py`, `make_copy.py`: unchanged copies. New: `app.py`, `client.py`, this README. ## Known limits / follow-ups - The cache on `/data` has no eviction (HK is ~4.6 GB). Job-private caches are deleted after the job. - `smart_project` UV layouts of converted text (fascia_text_*, neon_*) differ between Linux and Windows Blender. Each export's textures match its own GLB UVs, but the PNGs are not byte-comparable across platforms. - `scene.json` paths use `/` (relpaths on Linux). The local Windows export used `\`. `build_level.py` joins them with `os.path.join`, which works on both.unreal_fal_ue/README.md5.5 KBecd9f8cf65dbupstream ↗
# unreal_fal_ue: UE 5.8 headless on fal Serverless (`apek/unreal-build`) Status (2026-11-02): **working end to end.** HK builds on fal and renders with MRQ. Against the local Windows MRQ frames the result is near-identical: PSNR 47–50 dB and a mean difference of 0.25–0.36 out of 255. ## Endpoints (private app `apek/unreal-build`, GPU-RTX5090) | route | in | out | |---|---|---| | `/smoke` | `{"render": true}` | engine version, GPU, Vulkan, and a lit-cube PNG rendered offscreen | | `/build` | `{"export_url", "scene": "HK", "level", "stages", "nanite_tris", "exposure_offset"}` | `content_url` (zip of `Content/AnyScene/<scene>`, Config, uproject), `log_url`, `report` | | `/render` | `{"content_url", "scene", "cameras": [...], "config": "MRQ_Still", "resolution": [1920,804], "frames": 1, "temporal_samples": 8, "warmup_frames": 64}` | per camera: PNG/EXR URLs, an MP4 when frames > 1, logs | ``` # PowerShell 5.1 strips quotes from arguments, so pass the args as @file.json. FAL_KEY comes from FAL_SERVERLESS_KEY. python -I tools/call.py apek/unreal-build build @build_args.json build.json python -I tools/call.py apek/unreal-build render @render_args.json render.json python -I tools/fetch.py apek/unreal-build <request_id> out.json --wait # re-attach to a request after a local crash ``` ## Measured on HK (2026-11-02) - `/build`: 1514 s end to end, broken down as: - fetch and unzip of the 4.7 GB export: 163 s - UE commandlet: 1054 s (textures 263 s, meshes 629 s, materials 135 s) - zip: 230 s - upload: 47 s The content zip is 4.15 GB. - `/render`, three cameras at 1920x804, 8 temporal samples and 64 warm-up frames: 1352 s, broken down as: - content fetch: 96 s - MRQ config: 17 s - `cam_street`: 1103 s, the first render, which compiles shaders into a cold DDC - `cam_table`: 64 s - `cam_wok`: 61 s - Cold start, measured from zero runners to a new runner answering `/smoke {"render": false}`: 10.8 s. That node already had the image cached. A pull onto a node without the image couldn't be forced, so it is not measured; expect minutes for 24 GB. - The shared DDC on `/data` persists across runners, so later renders on any runner skip most shader compiles. - Cost: an RTX 5090 is $0.000694/s, about $2.50/h. A warm runner (`min_concurrency=1`) costs about $60 a day. The app is now scaled to `min_concurrency=0` and `keep_alive=300`. To warm it again: `fal apps scale unreal-build --min-concurrency 1 --keep-alive 3600`. ## Container - The base image is `ghcr.io/epicgames/unreal-engine:dev-slim-5.8.4`: 24 GB compressed, with the engine at `/home/ue4/UnrealEngine` and user `ue4`. - Pulling it needs a GitHub account linked to Epic and in the EpicGames org, plus a classic PAT with `read:packages`. The PAT goes in the user env vars `GHCR_USER`/`GHCR_TOKEN` and is passed as `ContainerImage(registries=...)`; the Depot builder is the only thing that uses it. - On top of the base image: - Python 3.13 via uv (it must match the deploying Python); - ffmpeg, the Vulkan loader and tools, and the NVIDIA ICD JSON; - `NVIDIA_DRIVER_CAPABILITIES=all`. - The first image build took 644 s. Redeploys with an unchanged image take about 20 s. ``` $env:UE_SOURCE = "ghcr:dev-slim-5.8.4"; $env:GHCR_USER = "apekshik" # GHCR_TOKEN from the user env var uv run --python 3.13 --with fal==1.84.0 --with pydantic --with fal-client fal deploy app.py::UnrealBuild --app-name unreal-build --auth private --reset-scale ``` `UE_SOURCE=url:<linux engine zip>` builds from an Epic Linux binary zip instead. `none` builds a probe image with no engine. ## Linux specifics, now verified - UnrealEditor refuses to run as root, so every engine process runs as `ue4` via `setpriv`. - Rendering uses `-vulkan -RenderOffscreen` with `SDL_VIDEODRIVER=offscreen`; MRQ runs in `-game` mode with `-LevelSequence`. - Engine processes run in their own process group: - Shutdown is detected from log markers, and the group is killed EXIT_GRACE seconds later; `pkill -u ue4` follows. ShaderCompileWorker children otherwise keep stdout open after the engine has exited. - Commandlets never log `LogExit`: their last line is `Execution of commandlet took`. - `skip_retry_conditions=["timeout"]`. Without it, fal re-ran a timed-out build on a new runner every 2 h for 14 h. - Exports written on Windows hold backslash paths in scene.json. `build_level.py` converts them on Linux; see `_posix_paths`. ## Files (all new; nothing in `unreal/` changed) - `app.py`: the fal app. - `mrq_config.py`: makes a per-request MRQ config. It sets the output dir, resolution, frame range, samples and warm-up, and `use_camera_cut_for_warm_up=False`. - `smoke_shot.py`: an offscreen SceneCapture render of a lit cube. - `template/`: `AnySceneUE.uproject` and Config. `make_project()` swaps the startup map and adds Linux `SF_VULKAN_SM6`. - `tools/upload_export.py`, `tools/call.py`, `tools/fetch.py`: upload, submit-and-poll, and re-attach. - `build_level.py`: `unreal/build_level.py` as of 2026-11-01 20:25, plus `_posix_paths` (merge that upstream). `app.py` ships whichever `build_level.py` is in this folder. - `render_mrq.ps1`, `render_shots.py`, `TOGGLE.md`: unchanged copies. ## fal facts learned - `keep_alive` is capped at 3600 s without a support request. - Scaling settings persist across deploys; pass `--reset-scale` to apply the values in code. - A timeout makes the queue retry on a fresh runner unless `skip_retry_conditions` includes `"timeout"`. - `fal runners list --state terminated --since 6h` and `fal runners logs <id>` recover a dead runner's full log.web/README.md11 KBdb9c4bad0d88upstream ↗
# AnyScene web The showcase UI for AnyScene. The default product is the agent builder (`builder/`, e.g. the public `apek/anyscene`): drop a reference image and one request builds the whole scene on a fal GPU worker (up to fal's 2 h cap), with a Claude agent looping against an independent critic. The UI shows the agent's production log, its milestones as a schedule, the newest render live, the critic's score sheet round by round, and the result (stills, 360, package, cost). The older plan + build pipeline (`endpoint/`) is still there at `?mode=pipeline`. Styled as a film call sheet. Next.js (App Router, TypeScript), three.js, `@fal-ai/client` in the browser through `@fal-ai/server-proxy` on the server. The fal key never reaches the browser. ## Run ``` npm install cp .env.example .env.local # then fill in FAL_KEY and the app ids npm run env:local # (Windows) writes FAL_KEY into .env.local from the user env var FAL_SERVERLESS_KEY (never printed) npm run dev # http://localhost:3210 ``` The fal apps come from `NEXT_PUBLIC_*` variables in `.env.local`, as `<your-fal-username>/<app>` (see `.env.example`): `NEXT_PUBLIC_ANYSCENE_APP` (the builder), `NEXT_PUBLIC_ANYKIT_APP` (AnyKit, for the kit gallery), and optionally `NEXT_PUBLIC_ANYSCENE_PIPELINE_APP` (the older pipeline; defaults to the builder's id) and `NEXT_PUBLIC_ANYSCENE_TEST_APP` (a second builder the gallery shows with `?gallery_app=<id>`). There are no defaults: a live call to an unset app fails with a message naming the variable. `DEV_ORIGINS` (comma-separated) adds LAN hosts allowed to open the dev server. `npm run dev` and `npm run build` first copy `fixtures/` into `public/replay/` (`npm run fixtures`). ## Builder (default) - Live: **Build the scene** uploads the reference to fal storage and calls `<NEXT_PUBLIC_ANYSCENE_APP>/ref2scene` with `{reference_image_urls, prompt, effort, scene: "scene"}`. Stage events come from the queue logs (prefix `ANYSCENE_STAGE `, polled every 2 s, deduped by message). - Schedule: the milestones the agent reports (plan, props, layout, dressing, screens, materials, lighting, atmosphere, cameras, finals, plus any it invents), with ROLLING on the current one, WRAPPED and a duration once it moves on, and "take 2" when it returns to one after a critique. Then the critic's state and the package. - Production log (bottom): every `log` event (tool label and what it did), the agent's `thinking` notes, milestones with a thumbnail, critic lines; filter by notes, tools or milestones. The caption under the frame is the agent's latest note. - Frame: Live (newest `preview_url`, with a strip of recent frames you can hold), Compare (the plate against the critic's reference-angle still, then the first final), Contact sheet (every reported frame), Stills and 360 after the wrap. - Critic's report (right): the eight scores per round side by side with the change from the round before, lines under 7 marked, the verdict stamp, the summary and the ranked fixes; earlier rounds' fixes fold away. - Result: stills, package, scene.blend and scene.glb, agent cost, minutes, critic rounds, anything `missing`. ### Builder replay `/?replay=builder` plays `fixtures/builder/` (`events.jsonl` timed by each event's own `t`, `build.json` as the response); speeds 1x to 64x in the toolbar, `&auto=1` starts it. That fixture is **synthetic** (`npm run fixture:builder`): it follows the real schema, reuses the pipeline run's images, and its package link is the earlier pipeline package. To drop in a real run: ``` npm run import:builder -- <run dir> # -> fixtures/builder/ npm run import:builder -- <run dir> fal_mc_1 # or keep both: ?replay=fal_mc_1 ``` It copies `events.jsonl`, `build.json` and any `still_*.png` (served locally in place of the remote stills), plus a reference image. A run without `build.json` yet replays up to its last event and stops there. ## Password, who's building, and limits The proxy checks a shared password, verified on the server: set `ANYSCENE_ACCESS_KEY` in the server environment and every proxied request must carry it as `x-anyscene-key`. With the variable unset the proxy is open in `next dev` and **closed** in production, so a fresh deployment can't start builds on the account. `ANYSCENE_ADMIN_KEY`, given in place of the password, also skips the limits below. The page asks once for the password and an email or username (**Sign in** in the toolbar; kept in that browser's localStorage) and sends them to our own proxy only (`x-anyscene-key`, `x-anyscene-user`, `x-anyscene-name`). The proxy tags each `/ref2scene` build with `owner` (the email or username) (stored by `builder/app.py`, listed by `/builds`, left out of the retry fingerprint), so the gallery's **My builds** shows your own and **Everyone** shows all. A build's page is shareable as `/?open=<build id>` (**Copy link**). It is self-declared behind the shared password: a label, not authentication (see Google sign-in below). Limits, all server only: | variable | default | | |---|---|---| | `ANYSCENE_BUILDS_PER_DAY` | 3 | builds per email per UTC day; counted in a private Vercel Blob store when `BLOB_READ_WRITE_TOKEN` is set (else in memory) | | `ANYSCENE_JOBS_PER_HOUR` | 6 | new jobs (builds and shots) per IP per hour, in memory per instance: a backstop | | `ANYSCENE_OWNER_DOMAIN` | none | only emails at this domain may build, e.g. `fal.ai` (usernames are always allowed) | The build button notes the cost (about $60-100 a build) and what's left today, and asks before starting. `app/api/me` answers the page's "is my key right, who am I, what's left" check. ### Google sign-in (later) To replace the self-declared name with real accounts restricted to one Google Workspace domain: 1. Google Cloud console, the owner's project: **APIs & Services > OAuth consent screen**, user type **Internal** (only the Workspace's accounts can sign in). Then **Credentials > Create credentials > OAuth client ID**, type **Web application**, authorized redirect URI `https://studio.anyscene.org/api/auth/callback/google` (and `http://localhost:3210/api/auth/callback/google` for dev). 2. `npm install next-auth@5` and add `auth.ts` with the Google provider, `authorization: { params: { hd: "fal.ai" } }`, and a `signIn` callback that returns `profile.email_verified && profile.email.endsWith("@fal.ai")` (`hd` alone is only a hint); export `handlers` from `app/api/auth/[...nextauth]/route.ts`. 3. Vercel env: `AUTH_SECRET` (`npx auth secret`), `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET`, `AUTH_TRUST_HOST=true`. 4. In `lib/guard.ts`, take the owner from `await auth()` (session email and name) instead of the `x-anyscene-*` headers, and treat a session as the access key (keep `ANYSCENE_ADMIN_KEY` for the owner). In the page, replace the sign-in card with `signIn("google")`. ## Replay mode (pipeline) `/?replay=demo` plays the recorded pipeline run in `fixtures/` with its original timing. Click **Plan the scene**, then **Approve and build**. | param | | |---|---| | `replay=<name>` | `demo` is `fixtures/` itself; any subfolder `fixtures/<name>/` is another recording | | `speed=<n>` | 1 (real time) to 64; also switchable in the toolbar (1x to 16x) | | `auto=1` | plans on load and approves the build 3.5 s after the plan lands, for hands-off screen recordings | A recording is: `plan_events.jsonl` and `build_events.jsonl` (one `{"t": seconds, ...stage event}` per line, as written by `endpoint/client.py --events`), `plan.json`, `build.json` (the BuildResponse), `reference.png`, `floor_plan.png`, `blockout_reference.png`, `blockout_overview.png`, `comparison.png` and `still_*.png`. Recorded `preview_url`s that match a local file (floor plan, comparison, stills in `build.json` order) are served locally; the rest (layout and dressing previews, prop references, GLBs) still load from fal's CDN. Without `build_events.jsonl` the replay synthesizes a build from `plan.json`. The last two lines of `fixtures/plan_events.jsonl` are marked `"synthetic"`: the recorded plan run lost its final events when the client crashed, so `blockout done` and `done` were added. ## Live mode Without `?replay`, **Plan the scene** uploads the reference to fal storage and calls `<pipeline app>/plan` through `/api/fal/proxy`; **Approve and build** calls `<pipeline app>/build` with the plan id and tier. Stage events are read from the queue logs (lines prefixed `ANYSCENE_STAGE `, deduped by message). The proxy only forwards to the configured `NEXT_PUBLIC_*_APP` apps (`<app>/**`) and fal storage uploads, so a deployment's key can't be used for other endpoints. On Vercel, set `FAL_KEY`, `ANYSCENE_ACCESS_KEY` and the `NEXT_PUBLIC_*_APP` ids in the project environment. ## 360 and scene lighting (ahead of the endpoint) The UI already reads these optional fields, from events or the BuildResponse: | field | where | | |---|---|---| | `panorama_url` (+ `viewpoint`) | `rendering` event, one per viewpoint | equirectangular Cycles render, 2:1 | | `panorama_urls` | BuildResponse | the same, first = reference camera | | `scene_glb_url` | `packaging` event / BuildResponse | decimated scene glTF with KHR_lights_punctual | | `environment_url` | any event / BuildResponse | equirect .hdr or PNG of the scene's light for the prop viewer (else the first panorama) | Until real ones exist, `scripts/make-placeholders.mjs` builds `fixtures/panorama_ref.png`, `panorama_mid.png` (a ray-cast grey room from `plan.json`) and `fixtures/scene.glb` (the plan as boxes with its lights), and replay injects them where the endpoint will emit them, labelled "placeholder". ## Screens - Left: reference plate, direction (prompt, the ten dials, planning effort, build tier), the schedule. - Centre: the camera-report frame with Floor plan (drawing, or the prop map that fills in as meshes land), Grey-box, Live (newest `preview_url`), Compare (slider between the reference and the matching render), 3D (the selected prop's GLB at the plan's real size, with dashed bounds and a ground shadow, lit by the scene's environment map once there is one, studio light until then), 360 (drag-to-look panoramas per viewpoint, plus the scene glTF as a free-orbit set model lit by the reference-camera panorama; hidden unless the build provides them), and Dailies after the wrap (stills, timings, package). **Follow** moves the frame to whatever just arrived; picking a view turns it off. - Bottom: the prop manifest, one card per `prop.id`: IN SHOP while meshing, TRIPO or MESHY with a turntable once the mesh lands, FAILED, or STAND-IN if it never arrives. Click a card for the 3D view. - Right: the breakdown (room, setting, counts, lights, palette, dials), notes for a revision, Approve and build. Prop GLBs are 10-60 MB, so each card's turntable is rendered once into a sprite strip on a shared offscreen renderer and the model is freed; only the 3D view keeps a live model. `npm run screenshots` (pipeline) and `npm run screenshots:builder` (builder), with the dev server running, capture the main states into `screenshots/` with the installed Edge or Chrome.scripts/sync_from_anyshot.py1.9 KBfadfc47a3732upstream ↗
"""Copy the code AnyScene shares with AnyShot from a sibling AnyShot checkout (github.com/apekshik/anyshot). python scripts/sync_from_anyshot.py [path/to/anyshot] (default: ../anyshot next to this repo) AnyShot is the home of these files; edit them there, then run this. Each copy gets a one-line header naming its source. """ import os import sys REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # anyscene path: anyshot path FILES = { "builder/blender_gpu.py": "agent/blender_gpu.py", "camera/camrig.py": "camera/camrig.py", "camera/shoot.py": "camera/shoot.py", "camera/README.md": "camera/README.md", "farm/app.py": "farm/app.py", "pipeline/env.sh": "pipeline/env.sh", "pipeline/gpu_run.sh": "pipeline/gpu_run.sh", } def header(src_rel): text = f"synced from github.com/apekshik/anyshot {src_rel}" return f"<!-- {text} -->" if src_rel.endswith(".md") else f"# {text}" def main(): src_root = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPO, "..", "anyshot")) if not os.path.exists(os.path.join(src_root, "agent", "director.py")): sys.exit(f"not an AnyShot checkout: {src_root}") for dst_rel, src_rel in FILES.items(): lines = open(os.path.join(src_root, src_rel), encoding="utf-8").read().splitlines(keepends=True) at = 1 if lines and lines[0].startswith("#!") else 0 # keep a shebang first lines.insert(at, header(src_rel) + "\n") dst = os.path.join(REPO, dst_rel) os.makedirs(os.path.dirname(dst), exist_ok=True) old = open(dst, encoding="utf-8").read() if os.path.exists(dst) else None new = "".join(lines) if new != old: with open(dst, "w", encoding="utf-8", newline="\n") as f: f.write(new) print(f"{'updated' if new != old else 'same '} {dst_rel} <- {src_rel}") if __name__ == "__main__": main().gitattributes19 B6313b56c5784upstream ↗
* text=auto eol=lf.gitignore1.2 KBf7ac1973425cupstream ↗
# Build outputs and generated assets: large, downloaded separately *.blend *.blend1 *.glb *.vdb *.exr *.mp4 *.png *.jpg assets/ refs/ out/ cache/ scenes/*/props/ scenes/*/props_prebrand/ scenes/*/compare/ scenes/*/debrand/ scenes/*/out/ scenes/*/cache/ breakdown/*/ !breakdown/*.py !breakdown/*.md # other heavy formats that should never be committed *.mov *.webm *.fbx *.obj *.usd *.usdz *.hdr *.blend@ # small README images are the exception to *.png / *.jpg above !docs/media/ !docs/media/*.jpg !docs/media/*.png !docs/media/*.gif # Local state generate/spend.jsonl **/spend.jsonl logs/ *.log .gpu.queue/ .gpu.*_done __pycache__/ node_modules/ .env* !.env.example pipeline/env.local.sh *.pem *.key .secrets* credentials*.json .venv/ # Blender writes this into the working directory waveletNoiseTile.bin # Client output folders (responses, downloaded stills/videos, event logs) anyshot_out/ builder_out/ # Generation records (refs/**/*.json) hold local output paths; refs/ above already excludes them, this is explicit refs/**/*.json *.tsbuildinfo # Unreal build logs *.out unreal_fx/work/ # sample reference plates for the Studio picker !web/public/samples/ !web/public/samples/*.jpgblender_addon/fal_render_farm-1.0.0.zip16 KBbd0fa4ba18f1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
blender_addon/fal_render_farm/__init__.py14 KBbb72ed2b6643upstream ↗
"""fal Render Farm: render the open scene on a fal Serverless Blender farm (farm/app.py, deployed on your own fal account) instead of the local GPU. Properties > Render > fal Render Farm. The open file is copied (never re-saved), packed in a separate Blender, zipped with a GPU script, uploaded to fal storage and rendered in chunks on up to N GPUs. Frames come back into the scene's own output path with the names a local render would give them. Talks to fal over plain HTTPS (urllib) only. """ bl_info = { "name": "fal Render Farm", "author": "AnyScene contributors", "version": (1, 0, 0), "blender": (4, 2, 0), "location": "Properties > Render > fal Render Farm", "description": "Render the open scene on a fal Serverless GPU render farm", "category": "Render", } import os import time import bpy from bpy.props import BoolProperty, IntProperty, StringProperty from . import fal_api, job as farm_job PKG = __package__ def prefs(): return bpy.context.preferences.addons[PKG].preferences class FALRF_Preferences(bpy.types.AddonPreferences): bl_idname = PKG fal_key: StringProperty( name="fal key", subtype="PASSWORD", default="", description="fal API key of the account that owns the farm app (key_id:key_secret). Stored in Blender's " "preferences; never printed or logged") app_id: StringProperty( name="Farm app", default="", description="fal app id of your render farm, <your-fal-username>/blender-farm-2 (farm/app.py deployed on your " "account). An app deployed as blender-farm (the older build) returns PNG frames only; " "blender-farm-2 also returns EXR, JPEG, TIFF and other image formats") chunk_size: IntProperty(name="Frames per chunk", default=24, min=1, max=1000, description="Frames rendered by one farm request (one GPU)") concurrency: IntProperty(name="Concurrent chunks", default=100, min=1, max=200, description="Requests in flight at once (the farm caps at its own max workers)") timeout_s: IntProperty(name="Chunk timeout (s)", default=3000, min=60, max=3500, description="The farm kills Blender after this long on one chunk") encode_mp4: BoolProperty(name="Encode MP4 when done", default=True, description="After an animation finishes, encode the frames to an H.264 MP4 next to " "them if ffmpeg is found (PNG/JPEG/TIFF/... frames only)") ffmpeg_path: StringProperty(name="ffmpeg", subtype="FILE_PATH", default="", description="Path to ffmpeg; empty = look it up on PATH") def draw(self, context): col = self.layout.column() col.prop(self, "fal_key") col.prop(self, "app_id") row = col.row() row.prop(self, "chunk_size") row.prop(self, "concurrency") col.prop(self, "timeout_s") row = col.row() row.prop(self, "encode_mp4") row.prop(self, "ffmpeg_path", text="") col.operator(FALRF_OT_check_key.bl_idname, icon="KEY_HLT") # -- operators -------------------------------------------------------------------------------------------------- def _busy(): j = farm_job.CURRENT return j is not None and j.running class FALRF_OT_render(bpy.types.Operator): """Render on the fal render farm instead of the local GPU""" bl_idname = "fal_render_farm.render" bl_label = "Render on fal" bl_options = {"REGISTER"} mode: bpy.props.EnumProperty(items=[("ANIMATION", "Animation", "The scene's frame range"), ("FRAME", "Frame", "The current frame")], default="ANIMATION") @classmethod def poll(cls, context): if _busy(): cls.poll_message_set("A farm render is already running") return False if context.scene is None: return False return True @classmethod def description(cls, context, props): if props.mode == "FRAME": return "Render the current frame on the fal render farm; it is saved to the output path" return "Render the scene's frame range on the fal render farm, into the output path" def execute(self, context): try: p = farm_job.plan(context, self.mode, prefs()) farm_job.save_copy(p) except farm_job.PlanError as e: self.report({"ERROR"}, str(e)) return {"CANCELLED"} farm_job.CURRENT = farm_job.Job(p).start() _start_ui_timer() self.report({"INFO"}, f"fal render farm: {len(p.frames)} frame(s) of '{p.scene_name}' on {p.app}") return {"FINISHED"} class FALRF_OT_cancel(bpy.types.Operator): """Cancel the running farm render (in-flight chunks are cancelled on fal; finished frames stay on disk)""" bl_idname = "fal_render_farm.cancel" bl_label = "Cancel" @classmethod def poll(cls, context): return _busy() def execute(self, context): farm_job.CURRENT.cancel() return {"FINISHED"} class FALRF_OT_retry(bpy.types.Operator): """Re-submit the chunks that failed, reusing the uploaded bundle""" bl_idname = "fal_render_farm.retry" bl_label = "Retry failed chunks" @classmethod def poll(cls, context): j = farm_job.CURRENT return j is not None and not j.running and j.bundle_url and any( c.status in ("failed", "cancelled") for c in j.chunks) def execute(self, context): if farm_job.CURRENT.retry_failed(): _start_ui_timer() return {"FINISHED"} class FALRF_OT_check_key(bpy.types.Operator): """Check that fal accepts the key (a status read on the farm app; nothing is rendered)""" bl_idname = "fal_render_farm.check_key" bl_label = "Check key" def execute(self, context): pr = prefs() if not pr.fal_key.strip(): self.report({"ERROR"}, "No key set") return {"CANCELLED"} if not pr.app_id.strip().strip("/"): self.report({"ERROR"}, farm_job.NO_APP_MSG) return {"CANCELLED"} ok, msg = fal_api.check_key(pr.fal_key.strip(), pr.app_id) self.report({"INFO"} if ok else {"ERROR"}, msg) return {"FINISHED"} if ok else {"CANCELLED"} class FALRF_OT_open_output(bpy.types.Operator): """Open the folder the frames are written to""" bl_idname = "fal_render_farm.open_output" bl_label = "Open output folder" def execute(self, context): path = bpy.path.abspath(context.scene.render.frame_path(frame=context.scene.frame_start)) folder = os.path.dirname(os.path.abspath(path)) os.makedirs(folder, exist_ok=True) bpy.ops.wm.path_open(filepath=folder) return {"FINISHED"} # -- panel ------------------------------------------------------------------------------------------------------ def _progress_text(s): c = s["counts"] done = c.get("done", 0) parts = [f"{done}/{s['n_chunks']} chunks", f"{s['frames_done']}/{s['frames_total']} frames", farm_job.fmt_s(s["elapsed"])] if s["gpu_s"]: parts.append(f"{s['gpu_s']:.0f} GPU-s (~${s['gpu_s'] / 3600 * farm_job.GPU_RATE_PER_HOUR:.2f})") return " · ".join(parts) class FALRF_PT_panel(bpy.types.Panel): bl_label = "fal Render Farm" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" def draw_header(self, context): self.layout.label(icon="WORLD") def draw(self, context): layout = self.layout scene = context.scene pr = prefs() frames = farm_job.frame_list(scene, "ANIMATION") est = farm_job.estimate(scene, len(frames), pr.chunk_size, pr.concurrency) samples, unit = farm_job.samples_of(scene) r = scene.render w = int(r.resolution_x * r.resolution_percentage / 100) h = int(r.resolution_y * r.resolution_percentage / 100) box = layout.box() col = box.column(align=True) col.label(text=f"Frames {scene.frame_start}-{scene.frame_end}" + (f" step {scene.frame_step}" if scene.frame_step > 1 else "") + f": {len(frames)} · {est['chunks']} chunks of {pr.chunk_size}", icon="RENDER_ANIMATION") col.label(text=f"{w}x{h}" + (f" · {samples} {unit}" if samples else "") + f" · {r.engine.replace('BLENDER_', '').title()} · {r.image_settings.file_format}") lo, hi = est["cost"], est["wall"] col.label(text=f"Estimate: ${lo[0]:.2f}-{lo[1]:.2f} · {farm_job.fmt_s(est['gpu_s'][0])}-" f"{farm_job.fmt_s(est['gpu_s'][1])} of GPU time", icon="INFO") col.label(text=f" ~{farm_job.fmt_s(hi[0])}-{farm_job.fmt_s(hi[1])} wall at {pr.concurrency} GPUs, " f"+ ~{farm_job.fmt_s(est['cold_start'])} cold start") col.label(text=f" {est['sec_per_frame'][0]:.1f}-{est['sec_per_frame'][1]:.1f} s/frame, " f"${farm_job.GPU_RATE_PER_HOUR:.2f}/GPU-h (RTX PRO 6000); an estimate") if not pr.fal_key.strip(): layout.label(text="Set your fal key in the add-on preferences", icon="ERROR") if not pr.app_id.strip().strip("/"): layout.label(text="Set the farm app (<your-fal-username>/blender-farm-2) below", icon="ERROR") if r.image_settings.file_format != "PNG" and pr.app_id.strip().strip("/").endswith("/blender-farm"): layout.label(text=f"{pr.app_id} returns PNG only; use a blender-farm-2 deployment for " f"{r.image_settings.file_format}", icon="ERROR") if r.image_settings.file_format == "FFMPEG": layout.label(text="Video output: pick an image format (MP4 is encoded after)", icon="ERROR") row = layout.row(align=True) row.scale_y = 1.4 row.operator(FALRF_OT_render.bl_idname, text="Render animation on fal", icon="RENDER_ANIMATION").mode = "ANIMATION" row = layout.row(align=True) row.operator(FALRF_OT_render.bl_idname, text="Render this frame on fal", icon="RENDER_STILL").mode = "FRAME" row.operator(FALRF_OT_cancel.bl_idname, text="Cancel", icon="CANCEL") sub = layout.column(align=True) sub.prop(pr, "app_id") row = sub.row(align=True) row.prop(pr, "chunk_size", text="Chunk") row.prop(pr, "concurrency", text="GPUs") sub.prop(pr, "encode_mp4") layout.label(text="Output: " + (r.filepath or "(blend file folder)"), icon="FILE_FOLDER") j = farm_job.CURRENT if j is None: return s = j.snapshot() box = layout.box() icon = {"done": "CHECKMARK", "failed": "ERROR", "cancelled": "CANCEL"}.get(s["phase"], "SORTTIME") box.label(text=f"{s['phase'].title()} · {s['app']}", icon=icon) if s["n_chunks"]: box.progress(factor=s["frames_done"] / max(1, s["frames_total"]), text=_progress_text(s)) c = s["counts"] box.label(text=f"queued {c.get('queued', 0) + c.get('pending', 0)} · rendering {c.get('rendering', 0)} " f"· downloading {c.get('downloading', 0)} · done {c.get('done', 0)} " f"· failed {c.get('failed', 0)}" + (f" · {s['skipped']} frames already on disk" if s["skipped"] else "")) if s["message"]: box.label(text=s["message"][:160]) for label, err in s["failed"][:8]: box.label(text=f"chunk {label}: {err[:140]}", icon="ERROR") if s["mp4"]: box.label(text=f"MP4: {s['mp4']}", icon="FILE_MOVIE") row = box.row() row.operator(FALRF_OT_retry.bl_idname, icon="FILE_REFRESH") row.operator(FALRF_OT_open_output.bl_idname, icon="FILE_FOLDER") def draw_status(self, context): j = farm_job.CURRENT if j is None or (not j.running and j.finished_seen and time.time() - (j.t_end or 0) > 30): return s = j.snapshot() text = f"fal farm: {s['phase']}" if s["n_chunks"]: text += " · " + _progress_text(s) self.layout.separator() self.layout.label(text=text, icon="WORLD") # -- UI timer: redraws while a job runs, then reacts once to its end -------------------------------------------- def _redraw(): wm = bpy.context.window_manager if wm is None: return for win in wm.windows: for area in win.screen.areas: if area.type in ("PROPERTIES", "STATUSBAR"): area.tag_redraw() def _tick(): j = farm_job.CURRENT if j is None: return None try: _redraw() except Exception: # noqa: BLE001 - never let a redraw problem kill the timer pass if j.running: return 0.5 if not j.finished_seen: j.finished_seen = True if j.p.mode == "FRAME" and j.phase == "done": _show_frame(j) return None def _show_frame(j): path = j.p.targets[j.p.frames[0]] if not os.path.exists(path): return img = bpy.data.images.load(path, check_existing=True) img.reload() for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type == "IMAGE_EDITOR": area.spaces.active.image = img area.tag_redraw() return def _start_ui_timer(): if not bpy.app.timers.is_registered(_tick): bpy.app.timers.register(_tick, first_interval=0.5, persistent=True) classes = (FALRF_Preferences, FALRF_OT_render, FALRF_OT_cancel, FALRF_OT_retry, FALRF_OT_check_key, FALRF_OT_open_output, FALRF_PT_panel) def register(): for c in classes: bpy.utils.register_class(c) bpy.types.STATUSBAR_HT_header.append(draw_status) def unregister(): if farm_job.CURRENT is not None and farm_job.CURRENT.running: farm_job.CURRENT.cancel() if bpy.app.timers.is_registered(_tick): bpy.app.timers.unregister(_tick) bpy.types.STATUSBAR_HT_header.remove(draw_status) for c in reversed(classes): bpy.utils.unregister_class(c)blender_addon/fal_render_farm/blender_manifest.toml531 B6bbcfbeecef7upstream ↗
schema_version = "1.0.0" id = "fal_render_farm" version = "1.0.0" name = "fal Render Farm" tagline = "Render the open scene on a fal Serverless GPU render farm" maintainer = "AnyScene contributors" type = "add-on" blender_version_min = "4.2.0" license = ["SPDX:GPL-3.0-or-later"] tags = ["Render"] [permissions] network = "Upload the scene to fal storage and render it on fal Serverless" files = "Save a packed temp copy and write frames to the output path" [build] paths_exclude_pattern = ["__pycache__/", "*.pyc", "/.git/"]blender_addon/fal_render_farm/fal_api.py6.5 KB55f94edebdbeupstream ↗
"""fal over plain HTTPS (urllib only, nothing Blender doesn't ship): the queue API and CDN storage uploads. Queue: POST https://queue.fal.run/<app> -> {request_id, status_url, response_url, cancel_url} GET status_url -> {status: IN_QUEUE | IN_PROGRESS | COMPLETED, queue_position?} GET response_url -> the app's result (or an HTTP error with the app's exception) PUT cancel_url Storage: the same CDN v3 flow fal_client uses: a short-lived upload token from rest.fal.ai, then a single POST (small files) or a multipart upload (big ones) to the token's base URL. Returns a public access URL. The key is only ever put in an Authorization header; it never appears in an error message or a log line. """ import json import os import time import urllib.error import urllib.request QUEUE_URL = "https://queue.fal.run" REST_URL = "https://rest.fal.ai" USER_AGENT = "fal-render-farm-blender/1.0" MULTIPART_THRESHOLD = 90 * 1024 * 1024 PART_SIZE = 10 * 1024 * 1024 TRANSIENT = {408, 409, 425, 429, 500, 502, 503, 504} class FalError(RuntimeError): def __init__(self, msg, status=None): super().__init__(msg) self.status = status def _scrub(text, key): text = text or "" if key: text = text.replace(key, "[key]") return text def _request(method, url, *, key=None, auth=None, body=None, headers=None, timeout=60, retries=3, raw=False): """One HTTP call with retries on transient failures. `auth` is a full Authorization header value.""" hdrs = {"User-Agent": USER_AGENT, "Accept": "application/json"} if key: hdrs["Authorization"] = f"Key {key}" if auth: hdrs["Authorization"] = auth if isinstance(body, (dict, list)): body = json.dumps(body).encode() hdrs["Content-Type"] = "application/json" hdrs.update(headers or {}) last = None for attempt in range(retries): req = urllib.request.Request(url, data=body, method=method, headers=hdrs) try: with urllib.request.urlopen(req, timeout=timeout) as r: data = r.read() if raw: return data, dict(r.headers) return json.loads(data) if data else {} except urllib.error.HTTPError as e: detail = _scrub(e.read().decode("utf-8", "replace"), key) last = FalError(f"HTTP {e.code} from {method} {url.split('?')[0]}: {detail[:2000]}", e.code) if e.code not in TRANSIENT: raise last from None except (urllib.error.URLError, TimeoutError, ConnectionError, OSError) as e: last = FalError(f"{method} {url.split('?')[0]} failed: {_scrub(str(e), key)}") time.sleep(1.5 * (attempt + 1)) raise last # -- storage ---------------------------------------------------------------------------------------------------- def _cdn_token(key): data = _request("POST", f"{REST_URL}/storage/auth/token?storage_type=fal-cdn-v3", key=key, body={}) return f"{data['token_type']} {data['token']}", data["base_url"] def upload_file(key, path, content_type="application/zip", expires_s=7 * 86400, progress=None): """Upload a local file to fal storage and return its public URL. progress(bytes_sent, total) is optional.""" size = os.path.getsize(path) name = os.path.basename(path) auth, base = _cdn_token(key) lifecycle = json.dumps({"expiration_duration_seconds": int(expires_s)}) if size <= MULTIPART_THRESHOLD: with open(path, "rb") as f: data = f.read() res = _request("POST", f"{base}/files/upload", auth=auth, body=data, timeout=600, headers={ "Content-Type": content_type, "X-Fal-File-Name": name, "X-Fal-Object-Lifecycle": lifecycle}) if progress: progress(size, size) return res["access_url"] res = _request("POST", f"{base}/files/upload/multipart", auth=auth, headers={ "Content-Type": content_type, "X-Fal-File-Name": name, "X-Fal-Object-Lifecycle": lifecycle}) access, upload_id = res["access_url"], res["uploadId"] parts, sent = [], 0 with open(path, "rb") as f: n = 1 while chunk := f.read(PART_SIZE): _, hdrs = _request("PUT", f"{access}/multipart/{upload_id}/{n}", auth=auth, body=chunk, timeout=600, raw=True, headers={"Content-Type": content_type, "Accept-Encoding": "identity"}) etag = {k.lower(): v for k, v in hdrs.items()}.get("etag") parts.append({"partNumber": n, "etag": etag}) sent += len(chunk) if progress: progress(sent, size) n += 1 _request("POST", f"{access}/multipart/{upload_id}/complete", auth=auth, body={"parts": parts}) return access def download(url, dest, timeout=600): """Stream a URL to a file (written to dest.part, then renamed).""" tmp = dest + ".part" for attempt in range(3): try: req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=timeout) as r, open(tmp, "wb") as f: while chunk := r.read(4 << 20): f.write(chunk) os.replace(tmp, dest) return dest except (urllib.error.URLError, TimeoutError, ConnectionError, OSError) as e: err = e time.sleep(2 * (attempt + 1)) raise FalError(f"download failed: {err}") # -- queue ------------------------------------------------------------------------------------------------------ def submit(key, app, payload): return _request("POST", f"{QUEUE_URL}/{app.strip('/')}", key=key, body=payload) def status(key, handle): return _request("GET", handle["status_url"], key=key) def result(key, handle): return _request("GET", handle["response_url"], key=key, timeout=120) def cancel(key, handle): try: _request("PUT", handle["cancel_url"], key=key, retries=1, timeout=15) except FalError: pass # already finished, or never started: nothing to cancel def check_key(key, app): """Cheap auth probe: a status read of a request id that doesn't exist. 401/403 means a bad key.""" try: _request("GET", f"{QUEUE_URL}/{app.strip('/')}/requests/00000000-0000-0000-0000-000000000000/status", key=key, retries=1, timeout=15) except FalError as e: if e.status in (401, 403): return False, "fal rejected the key (HTTP %s)" % e.status return True, "key accepted" return True, "key accepted"blender_addon/fal_render_farm/farm_gpu.py1.1 KB07c94187b214upstream ↗
"""Shipped in every bundle and run with --python before the render: headless Blender on the farm would otherwise render Cycles on the CPU. Picks OptiX (else CUDA), enables only those devices, and puts every scene on the GPU now, on file load and at render start. Nothing else about the scene is touched. (Same idea as builder/blender_gpu.py.)""" import bpy from bpy.app.handlers import persistent def _devices(): try: prefs = bpy.context.preferences.addons["cycles"].preferences except (KeyError, AttributeError): return "no cycles" for kind in ("OPTIX", "CUDA"): try: prefs.compute_device_type = kind prefs.get_devices() except TypeError: continue if any(d.type == kind for d in prefs.devices): for d in prefs.devices: d.use = d.type == kind return kind return "none" @persistent def _gpu_scenes(*_): for sc in bpy.data.scenes: sc.cycles.device = "GPU" print("fal-farm: compute device", _devices()) _gpu_scenes() bpy.app.handlers.load_post.append(_gpu_scenes) bpy.app.handlers.render_init.append(_gpu_scenes)blender_addon/fal_render_farm/job.py21 KBb9ae39b99024upstream ↗
"""A farm render job: everything that touches bpy happens in plan() on the main thread; Job.run() is pure Python on a background thread (pack in a separate Blender, zip, upload, fan chunks out to the queue, download frames), so the UI never blocks. The UI reads Job.snapshot() from a timer.""" import concurrent.futures as cf import hashlib import math import os import re import shutil import subprocess import sys import tempfile import threading import time import traceback import zipfile from collections import deque from dataclasses import dataclass, field from . import fal_api HERE = os.path.dirname(os.path.abspath(__file__)) GPU_RATE_PER_HOUR = 4.00 # RTX PRO 6000 on fal REF_PIXELS = 1920 * 804 REF_SAMPLES = 48 REF_SEC_PER_FRAME = (5.0, 7.0) # measured on mission control at 1920x804 / 48 spp CHUNK_OVERHEAD_S = 20.0 # Blender start + scene load + zip/upload of the frames, per request COLD_START_S = 90.0 # first workers when the app has scaled to zero VIDEO_SAFE = {"PNG", "JPEG", "TIFF", "TARGA", "TARGA_RAW", "BMP", "WEBP"} NO_WINDOW = 0x08000000 if sys.platform == "win32" else 0 # CREATE_NO_WINDOW: no console flashing up per subprocess _uploads = {} # bundle sha256 -> fal storage URL, so a retry or re-render of an unchanged file skips the upload CURRENT = None # the job the panel shows class PlanError(Exception): pass NO_APP_MSG = ("Set the farm app in Preferences > Add-ons > fal Render Farm: the fal app id of your render farm, " "<your-fal-username>/blender-farm-2 (deploy farm/app.py on your own fal account)") # -- estimate --------------------------------------------------------------------------------------------------- def samples_of(scene): eng = scene.render.engine if eng == "CYCLES": return scene.cycles.samples, "spp" if eng.startswith("BLENDER_EEVEE"): return scene.eevee.taa_render_samples, "samples" return None, "" def frame_list(scene, mode): if mode == "FRAME": return [scene.frame_current] return list(range(scene.frame_start, scene.frame_end + 1, max(1, scene.frame_step))) def estimate(scene, n_frames, chunk, concurrency): """Rough cost/time from the reference rate, scaled by pixels x samples. An estimate, not a quote.""" r = scene.render pixels = r.resolution_x * r.resolution_y * (r.resolution_percentage / 100.0) ** 2 samples, _ = samples_of(scene) scale = pixels / REF_PIXELS * ((samples or REF_SAMPLES * 0.05) / REF_SAMPLES) spf = [max(0.3, s * scale) for s in REF_SEC_PER_FRAME] n_chunks = max(1, math.ceil(n_frames / max(1, chunk))) if n_frames else 0 gpu_s = [n_frames * s + n_chunks * CHUNK_OVERHEAD_S for s in spf] waves = math.ceil(n_chunks / max(1, min(concurrency, n_chunks or 1))) if n_chunks else 0 per = min(chunk, n_frames) wall = [waves * (per * s + CHUNK_OVERHEAD_S) for s in spf] return { "sec_per_frame": spf, "chunks": n_chunks, "gpu_s": gpu_s, "cost": [g / 3600 * GPU_RATE_PER_HOUR for g in gpu_s], "wall": wall, "cold_start": COLD_START_S, "pixels": int(pixels), "samples": samples, } def fmt_s(s): s = int(round(s)) if s < 60: return f"{s}s" if s < 3600: return f"{s // 60}m{s % 60:02d}s" return f"{s // 3600}h{(s % 3600) // 60:02d}m" # -- plan (main thread) ----------------------------------------------------------------------------------------- @dataclass class Plan: mode: str scene_name: str frames: list step: int targets: dict # frame -> absolute local path, exactly as a local render names it farm_pattern: str # the filename part of render.filepath, with its # padding file_format: str fps: float key: str app: str chunk_size: int concurrency: int timeout_s: int encode_mp4: bool ffmpeg: str blender_bin: str workdir: str = "" copy_path: str = "" skipped: list = field(default_factory=list) def plan(context, mode, prefs): import bpy scene = context.scene r = scene.render if not prefs.fal_key.strip(): raise PlanError("Set your fal key in Preferences > Add-ons > fal Render Farm") if not prefs.app_id.strip().strip("/"): raise PlanError(NO_APP_MSG) if r.image_settings.file_format == "FFMPEG" or getattr(r.image_settings, "media_type", "") == "VIDEO": raise PlanError("Output is a video format; the farm renders frame sequences in chunks. Pick an image format " "(PNG, EXR...) and enable 'Encode MP4' in the add-on preferences instead") if scene.camera is None: raise PlanError("The scene has no active camera") if r.filepath.startswith("//") and not bpy.data.filepath: raise PlanError("The output path is relative (//) but this file has never been saved; save it first " "or set an absolute output path") frames = frame_list(scene, mode) if not frames: raise PlanError("The frame range is empty") targets = {f: os.path.abspath(bpy.path.abspath(r.frame_path(frame=f))) for f in frames} pattern = re.split(r"[\\/]", r.filepath)[-1] if pattern in ("", ".", ".."): pattern = "" ffmpeg = prefs.ffmpeg_path.strip() or shutil.which("ffmpeg") or "" return Plan( mode=mode, scene_name=scene.name, frames=frames, step=max(1, scene.frame_step) if mode == "ANIMATION" else 1, targets=targets, farm_pattern=pattern, file_format=r.image_settings.file_format, fps=r.fps / (r.fps_base or 1.0), key=prefs.fal_key.strip(), app=prefs.app_id.strip().strip("/"), chunk_size=max(1, prefs.chunk_size), concurrency=max(1, prefs.concurrency), timeout_s=prefs.timeout_s, encode_mp4=prefs.encode_mp4 and mode == "ANIMATION", ffmpeg=ffmpeg, blender_bin=bpy.app.binary_path, ) def save_copy(p): """Write the open file, unsaved edits included, to a temp dir. copy=True leaves the user's file, its path and its dirty state alone; relative_remap keeps // paths pointing at the same files from the new location.""" import bpy p.workdir = tempfile.mkdtemp(prefix="fal_farm_") p.copy_path = os.path.join(p.workdir, "copy.blend") bpy.ops.wm.save_as_mainfile(filepath=p.copy_path, copy=True, relative_remap=True, compress=False, check_existing=False) if not os.path.exists(p.copy_path): raise PlanError("Could not save a temporary copy of the file") # -- job (background thread) ------------------------------------------------------------------------------------ @dataclass class Chunk: frames: list step: int status: str = "pending" # pending queued rendering downloading done failed cancelled error: str = "" attempts: int = 0 render_s: float = 0.0 gpu_s: float = 0.0 device: str = "" handle: dict = None @property def label(self): a, b = self.frames[0], self.frames[-1] return f"{a}" if a == b else f"{a}-{b}" class Job: def __init__(self, p: Plan): self.p = p self.lock = threading.Lock() self.cancel_ev = threading.Event() self.phase = "starting" # packing uploading rendering encoding done failed cancelled self.message = "" self.log = deque(maxlen=200) self.chunks = [] self.bundle_url = self.bundle_id = None self.t0 = time.time() self.t_end = None self.mp4 = None self.thread = None self.finished_seen = False # set by the UI once it has reacted to the end of the job # ---- public ---- def start(self): self.thread = threading.Thread(target=self._run, name="fal-render-farm", daemon=True) self.thread.start() return self def cancel(self): self.cancel_ev.set() self._say("cancelling...") with self.lock: handles = [c.handle for c in self.chunks if c.handle and c.status in ("queued", "rendering")] for h in handles: threading.Thread(target=fal_api.cancel, args=(self.p.key, h), daemon=True).start() def retry_failed(self): if self.running: return False with self.lock: failed = [c for c in self.chunks if c.status in ("failed", "cancelled")] for c in failed: c.status, c.error, c.attempts, c.handle = "pending", "", 0, None if not failed: return False self.cancel_ev.clear() self.t0, self.t_end, self.phase = time.time(), None, "starting" self.finished_seen = False self.thread = threading.Thread(target=self._run, name="fal-render-farm", daemon=True) self.thread.start() return True @property def running(self): return self.thread is not None and self.thread.is_alive() def wait(self, timeout=None): if self.thread: self.thread.join(timeout) return not self.running def snapshot(self): with self.lock: counts = {} for c in self.chunks: counts[c.status] = counts.get(c.status, 0) + 1 frames_done = sum(len(c.frames) for c in self.chunks if c.status == "done") return { "phase": self.phase, "message": self.message, "counts": counts, "n_chunks": len(self.chunks), "frames_done": frames_done + len(self.p.skipped), "frames_total": len(self.p.frames), "skipped": len(self.p.skipped), "elapsed": (self.t_end or time.time()) - self.t0, "gpu_s": sum(c.gpu_s for c in self.chunks), "render_s": sum(c.render_s for c in self.chunks), "failed": [(c.label, c.error) for c in self.chunks if c.status == "failed"], "mp4": self.mp4, "mode": self.p.mode, "app": self.p.app, } # ---- internals ---- def _say(self, msg): with self.lock: self.message = msg self.log.append(f"[{time.strftime('%H:%M:%S')}] {msg}") print("fal render farm:", msg) def _set_phase(self, phase, msg=None): with self.lock: self.phase = phase if msg: self._say(msg) def _run(self): try: if not self.chunks: missing = [f for f in self.p.frames if not os.path.exists(self.p.targets[f])] self.p.skipped = [f for f in self.p.frames if f not in set(missing)] if self.p.skipped: self._say(f"{len(self.p.skipped)} frames already on disk, skipped") self.chunks = self._split(missing) if not self.chunks: self._finish() return if not self.bundle_url: self._bundle() self._render() self._finish() except Exception as e: # noqa: BLE001 - any failure lands in the panel, not a stack trace on stderr only if self.cancel_ev.is_set(): self._set_phase("cancelled", "cancelled") else: self._set_phase("failed", f"failed: {fal_api._scrub(str(e), self.p.key)}") self.log.append(fal_api._scrub(traceback.format_exc(), self.p.key)) self.t_end = time.time() finally: if self.p.workdir and not self.running_chunks(): shutil.rmtree(self.p.workdir, ignore_errors=True) def running_chunks(self): return any(c.status in ("queued", "rendering", "downloading") for c in self.chunks) def _split(self, missing): """Contiguous runs (in steps of frame_step) of missing frames, cut into chunks of at most chunk_size.""" step, out, run = self.p.step, [], [] for f in missing: if run and (f != run[-1] + step or len(run) >= self.p.chunk_size): out.append(Chunk(run, step)) run = [] run.append(f) if run: out.append(Chunk(run, step)) return out def _bundle(self): p = self.p self._set_phase("packing", "packing a copy of the file (textures, fonts, linked data)...") packed = os.path.join(p.workdir, "scene.blend") proc = subprocess.run( [p.blender_bin, "-b", "--factory-startup", p.copy_path, "--python", os.path.join(HERE, "pack_copy.py"), "--", packed], capture_output=True, text=True, encoding="utf-8", errors="replace", creationflags=NO_WINDOW) out = proc.stdout + proc.stderr for line in out.splitlines(): if line.startswith(("PACK-WARNING:", "PACK-INFO:")): self.log.append(line) problems = [l[len("PACK-PROBLEM:"):].strip() for l in out.splitlines() if l.startswith("PACK-PROBLEM:")] if problems: raise RuntimeError("the file references data that can't be packed: " + "; ".join(problems[:6])) if proc.returncode != 0 or not os.path.exists(packed): raise RuntimeError(f"packing failed (Blender exited {proc.returncode}): {out[-800:]}") os.remove(p.copy_path) if self.cancel_ev.is_set(): raise RuntimeError("cancelled") zpath = os.path.join(p.workdir, "bundle.zip") with zipfile.ZipFile(zpath, "w", zipfile.ZIP_STORED) as z: # the .blend is saved compressed already z.write(packed, "scene.blend") z.write(os.path.join(HERE, "farm_gpu.py"), "farm_gpu.py") os.remove(packed) sha = hashlib.sha256() with open(zpath, "rb") as f: while b := f.read(8 << 20): sha.update(b) digest = sha.hexdigest() self.bundle_id = digest[:16] size = os.path.getsize(zpath) if digest in _uploads: self.bundle_url = _uploads[digest] self._say(f"bundle {self.bundle_id} ({size / 1e6:.1f} MB) already uploaded") else: self._set_phase("uploading", f"uploading bundle {self.bundle_id} ({size / 1e6:.1f} MB)...") def prog(sent, total): with self.lock: self.message = f"uploading bundle {self.bundle_id}: {sent / 1e6:.0f}/{total / 1e6:.0f} MB" self.bundle_url = fal_api.upload_file(p.key, zpath, progress=prog) _uploads[digest] = self.bundle_url self._say(f"bundle {self.bundle_id} uploaded") os.remove(zpath) def _args(self, c): p = self.p return ["{bundle}/scene.blend", "-S", p.scene_name, "--python", "{bundle}/farm_gpu.py", "-o", "{out}/frames/" + p.farm_pattern, "-s", str(c.frames[0]), "-e", str(c.frames[-1]), "-j", str(c.step), "-a"] def _render(self): pending = [c for c in self.chunks if c.status == "pending"] n = len(pending) self._set_phase("rendering", f"rendering {sum(len(c.frames) for c in pending)} frames in {n} chunks " f"on {self.p.app} ({min(n, self.p.concurrency)} at a time)") with cf.ThreadPoolExecutor(max_workers=min(n, self.p.concurrency), thread_name_prefix="fal-chunk") as pool: list(pool.map(self._chunk, pending)) def _chunk(self, c): p = self.p while not self.cancel_ev.is_set(): c.attempts += 1 try: self._chunk_once(c) return except Exception as e: # noqa: BLE001 err = fal_api._scrub(str(e), p.key) if "with 0 frames" in err and p.file_format != "PNG": err = (f"the farm returned no {p.file_format} files. {p.app} may only return PNGs; " f"set the farm app to your blender-farm-2 deployment. Blender said: {err[-600:]}") if c.handle and c.status in ("queued", "rendering"): fal_api.cancel(p.key, c.handle) # e.g. a status poll gave up: don't leave a twin rendering with self.lock: c.error, c.handle = err, None if self.cancel_ev.is_set(): break if c.attempts >= 2: with self.lock: c.status = "failed" self._say(f"chunk {c.label} failed: {err[:300]}") return self._say(f"chunk {c.label} attempt {c.attempts} failed, retrying: {err[:200]}") time.sleep(3) with self.lock: c.status = "cancelled" def _chunk_once(self, c): p = self.p payload = {"bundle_url": self.bundle_url, "bundle_id": self.bundle_id, "blender_args": self._args(c), "timeout_s": int(p.timeout_s), "outputs": ["frames/**/*"]} with self.lock: c.status = "queued" handle = fal_api.submit(p.key, p.app, payload) with self.lock: c.handle = handle delay = 1.0 while True: if self.cancel_ev.is_set(): fal_api.cancel(p.key, handle) raise RuntimeError("cancelled") st = fal_api.status(p.key, handle).get("status") if st == "IN_PROGRESS": with self.lock: c.status = "rendering" elif st == "COMPLETED": break time.sleep(delay) delay = min(delay * 1.5, 4.0) res = fal_api.result(p.key, handle) if "frames_url" not in res: raise RuntimeError(f"unexpected farm response: {str(res)[:500]}") with self.lock: c.status = "downloading" zpath = os.path.join(tempfile.gettempdir(), f"fal_farm_{self.bundle_id}_{c.frames[0]}_{c.frames[-1]}.zip") fal_api.download(res["frames_url"], zpath) try: self._extract(zpath, c) finally: os.remove(zpath) with self.lock: c.status, c.error, c.handle = "done", "", None c.render_s = float(res.get("render_seconds", 0)) c.gpu_s = c.render_s + float(res.get("bundle_seconds", 0)) c.device = res.get("device", "") self._say(f"chunk {c.label}: {res.get('frame_count')} frames, render {c.render_s:.0f}s on " f"{c.device.split(',')[0]}") def _extract(self, zpath, c): p = self.p by_name = {os.path.basename(p.targets[f]): p.targets[f] for f in c.frames} by_lower = {k.lower(): v for k, v in by_name.items()} fallback_dir = os.path.dirname(p.targets[c.frames[0]]) with zipfile.ZipFile(zpath) as z: for info in z.infolist(): if info.is_dir(): continue name = info.filename.rsplit("/", 1)[-1] dest = by_name.get(name) or by_lower.get(name.lower()) or os.path.join(fallback_dir, name) os.makedirs(os.path.dirname(dest), exist_ok=True) with z.open(info) as src, open(dest + ".part", "wb") as dst: shutil.copyfileobj(src, dst, 4 << 20) os.replace(dest + ".part", dest) missing = [f for f in c.frames if not os.path.exists(p.targets[f])] if missing: got = ", ".join(n.rsplit("/", 1)[-1] for n in zipfile.ZipFile(zpath).namelist()[:4]) raise RuntimeError(f"farm output is missing frames {missing[:8]} (got {got})") def _finish(self): if self.cancel_ev.is_set(): self._set_phase("cancelled", "cancelled") elif any(c.status == "failed" for c in self.chunks): n = sum(c.status == "failed" for c in self.chunks) self._set_phase("failed", f"{n} chunk(s) failed; fix and press Retry failed chunks") else: if self.p.encode_mp4: self._encode() self._set_phase("done", f"done: {len(self.p.frames)} frames in " f"{os.path.dirname(self.p.targets[self.p.frames[0]])}") self.t_end = time.time() def _encode(self): p = self.p if not p.ffmpeg: self._say("ffmpeg not found; skipping the MP4 (set its path in the add-on preferences)") return if p.file_format not in VIDEO_SAFE: self._say(f"skipping the MP4: {p.file_format} frames are linear/HDR, encode them in your grade instead") return self._set_phase("encoding", "encoding MP4...") first, last = p.frames[0], p.frames[-1] stem = os.path.splitext(os.path.basename(p.targets[first]))[0] stem = re.sub(r"\d+$", "", stem).rstrip("_.- ") or "render" out = os.path.join(os.path.dirname(p.targets[first]), f"{stem}_{first:04d}-{last:04d}.mp4") listing = os.path.join(tempfile.gettempdir(), f"fal_farm_concat_{os.getpid()}.txt") with open(listing, "w", encoding="utf-8") as f: for fr in p.frames: f.write("file '%s'\nduration %.6f\n" % (p.targets[fr].replace("\\", "/").replace("'", r"'\''"), 1 / p.fps)) f.write("file '%s'\n" % p.targets[last].replace("\\", "/").replace("'", r"'\''")) cmd = [p.ffmpeg, "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", listing, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-r", f"{p.fps:g}", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "16", "-preset", "slow", out] proc = subprocess.run(cmd, capture_output=True, text=True, errors="replace", creationflags=NO_WINDOW) os.remove(listing) if proc.returncode == 0: self.mp4 = out self._say(f"MP4: {out}") else: self._say(f"ffmpeg failed: {proc.stderr[-400:]}")blender_addon/fal_render_farm/pack_copy.py2.0 KB633803d1475eupstream ↗
"""Run by the add-on in a separate background Blender, on a temporary copy of the user's file (never the file itself): blender -b --factory-startup <copy.blend> --python pack_copy.py -- <out.blend> Makes linked library data local, packs images/fonts/sounds/volumes, and refuses (exit 2) if anything the render needs still points outside the file, listing each path on a "PACK-PROBLEM:" line. """ import os import sys import bpy out = sys.argv[sys.argv.index("--") + 1] if bpy.data.libraries: print("PACK-INFO: making linked data local:", ", ".join(l.filepath for l in bpy.data.libraries)) try: bpy.ops.object.make_local(type="ALL") except RuntimeError as e: print("PACK-INFO: make_local:", e) try: bpy.ops.file.pack_all() except RuntimeError as e: # missing files: reported below as problems print("PACK-INFO: pack_all:", str(e).strip()) problems, warnings = [], [] for img in bpy.data.images: if img.users and img.source in {"FILE", "SEQUENCE", "TILED", "MOVIE"} and not img.packed_file and img.filepath: problems.append(f"image not packed ({img.source.lower()}): {img.name} -> {img.filepath}") for v in bpy.data.volumes: if v.users and not v.packed_file and v.filepath: problems.append(f"volume not packed: {v.name} -> {v.filepath}") for c in bpy.data.cache_files: if c.users: problems.append(f"cache file (Alembic/USD) can't be packed: {c.name} -> {c.filepath}") for lib in bpy.data.libraries: problems.append(f"library still linked: {lib.filepath}") for ob in bpy.data.objects: for m in ob.modifiers: if m.type == "FLUID" and m.fluid_type == "DOMAIN": warnings.append(f"fluid domain {ob.name}: its disk cache is not shipped " f"({bpy.path.abspath(m.domain_settings.cache_directory)})") for w in warnings: print("PACK-WARNING:", w) for p in problems: print("PACK-PROBLEM:", p) if problems: sys.exit(2) bpy.ops.wm.save_as_mainfile(filepath=os.path.abspath(out), copy=True, compress=True) print("PACK-OK:", os.path.getsize(out))blender_addon/tests/make_test_scene.py4.1 KBcb97ea8fd861upstream ↗
"""Build the add-on's test scene: a few animated objects, an external image texture (to exercise packing), a sun and an area light, a moving camera, 48 frames at 640x360, Cycles at 32 spp with a fixed seed and no denoiser. blender -b --factory-startup --python make_test_scene.py -- <dir> Writes <dir>/test_scene.blend and <dir>/tex/checker.png; output path is //renders/test_#### (relative on purpose). """ import math import os import sys import bpy out_dir = os.path.abspath(sys.argv[sys.argv.index("--") + 1]) os.makedirs(os.path.join(out_dir, "tex"), exist_ok=True) bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene scene.name = "FarmTest" # external texture, saved next to the .blend and referenced relatively -> must be packed for the farm tex_path = os.path.join(out_dir, "tex", "checker.png") img = bpy.data.images.new("checker", 256, 256) px = [] for y in range(256): for x in range(256): c = 0.9 if ((x // 32) + (y // 32)) % 2 else 0.1 px += [c, 0.35 + 0.5 * (x / 255), 1.0 - c * 0.6, 1.0] img.pixels = px img.filepath_raw = tex_path img.file_format = "PNG" img.save() def material(name, color=None, image=None): m = bpy.data.materials.new(name) m.use_nodes = True bsdf = m.node_tree.nodes["Principled BSDF"] if image: t = m.node_tree.nodes.new("ShaderNodeTexImage") t.image = bpy.data.images.load(tex_path) m.node_tree.links.new(t.outputs["Color"], bsdf.inputs["Base Color"]) else: bsdf.inputs["Base Color"].default_value = (*color, 1) bsdf.inputs["Roughness"].default_value = 0.35 return m bpy.data.images.remove(img) # the material loads it back from disk, as a normal external image bpy.ops.mesh.primitive_plane_add(size=20) bpy.context.object.data.materials.append(material("floor", image=True)) bpy.ops.mesh.primitive_cube_add(size=1.5, location=(-2, 0, 0.75)) cube = bpy.context.object cube.data.materials.append(material("red", (0.8, 0.1, 0.08))) bpy.ops.mesh.primitive_uv_sphere_add(radius=0.9, location=(0.5, 1.5, 0.9)) sphere = bpy.context.object bpy.ops.object.shade_smooth() sphere.data.materials.append(material("blue", (0.1, 0.3, 0.9))) bpy.ops.mesh.primitive_monkey_add(size=1.4, location=(2.2, -0.8, 1.0)) monkey = bpy.context.object bpy.ops.object.shade_smooth() monkey.data.materials.append(material("gold", (0.9, 0.65, 0.2))) for f, z_rot, sz in ((1, 0, 0.9), (48, 2 * math.pi, 2.6)): scene.frame_set(f) cube.rotation_euler.z = z_rot cube.keyframe_insert("rotation_euler", index=2) sphere.location.z = sz sphere.keyframe_insert("location", index=2) monkey.rotation_euler.z = -z_rot / 2 monkey.keyframe_insert("rotation_euler", index=2) bpy.ops.object.light_add(type="SUN", rotation=(math.radians(50), 0, math.radians(30))) bpy.context.object.data.energy = 3 bpy.ops.object.light_add(type="AREA", location=(-3, -4, 4)) bpy.context.object.data.energy = 400 bpy.context.object.data.size = 3 bpy.ops.object.camera_add(location=(0, -9, 4)) cam = bpy.context.object scene.camera = cam for f, x in ((1, -2.0), (48, 2.0)): cam.location.x = x cam.keyframe_insert("location", index=0, frame=f) con = cam.constraints.new("TRACK_TO") bpy.ops.object.empty_add(location=(0, 0, 0.8)) con.target = bpy.context.object world = bpy.data.worlds.new("World") world.use_nodes = True world.node_tree.nodes["Background"].inputs[0].default_value = (0.05, 0.07, 0.1, 1) world.node_tree.nodes["Background"].inputs[1].default_value = 1.0 scene.world = world r = scene.render r.engine = "CYCLES" scene.cycles.samples = 32 scene.cycles.use_adaptive_sampling = False scene.cycles.use_denoising = False scene.cycles.seed = 0 r.resolution_x, r.resolution_y, r.resolution_percentage = 640, 360, 100 r.fps = 24 scene.frame_start, scene.frame_end = 1, 48 r.image_settings.file_format = "PNG" r.image_settings.color_mode = "RGBA" r.filepath = "//renders/test_####" scene.frame_set(1) bpy.ops.wm.save_as_mainfile(filepath=os.path.join(out_dir, "test_scene.blend"), relative_remap=True) bpy.ops.file.make_paths_relative() # the texture becomes //tex/checker.png, like a real project bpy.ops.wm.save_mainfile() print("TEST-SCENE:", os.path.join(out_dir, "test_scene.blend"))blender_addon/tests/test_farm.py12 KB4131624fc170upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
blender_addon/tests/test_zip_install.py1.4 KB9bb1d8b8bac8upstream ↗
"""Install the built extension zip the way a user would, into a throwaway user profile, and check it registers. BLENDER_USER_RESOURCES=<empty temp dir> blender -b --factory-startup --python-exit-code 1 \ --python test_zip_install.py -- blender_addon/fal_render_farm-1.0.0.zip """ import os import sys import bpy zip_path = os.path.abspath(sys.argv[sys.argv.index("--") + 1]) ext_dir = bpy.utils.user_resource("EXTENSIONS") assert os.environ.get("BLENDER_USER_RESOURCES") and os.path.normcase(os.path.abspath(ext_dir)).startswith(os.path.normcase(os.path.abspath(os.environ["BLENDER_USER_RESOURCES"]))), \ f"refusing to install into the real profile ({ext_dir})" print("ZIP-TEST extensions dir:", ext_dir) res = bpy.ops.extensions.package_install_files(repo="user_default", filepath=zip_path, enable_on_install=True) print("ZIP-TEST install:", res) name = "bl_ext.user_default.fal_render_farm" assert name in bpy.context.preferences.addons, list(bpy.context.preferences.addons.keys()) prefs = bpy.context.preferences.addons[name].preferences assert prefs.app_id == "" and prefs.chunk_size == 24 and prefs.concurrency == 100, "defaults" assert hasattr(bpy.types, "FALRF_PT_panel"), "panel not registered" assert bpy.ops.fal_render_farm.render.poll(), "render op poll" assert not bpy.ops.fal_render_farm.cancel.poll(), "cancel op poll" print("ZIP-TEST OK: installed and enabled as", name, "with defaults", prefs.app_id, prefs.chunk_size, prefs.concurrency)breakdown/anyshot_supercut.py10 KBbd0c0f748fc0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/assemble.py15 KBa2d830595377upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/builder_reel.py11 KB3be469e17466upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/kit_motion.py12 KBe3a9114509a4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/kit_supercut.py15 KB11100d456e8dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/promo.py6.2 KBced67aa13f62upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/site_rec.mjs7.5 KB2c77885a13a0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/SPEC.md3.8 KBe061e8715af6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/supercut.py17 KBf294fa8179e0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
breakdown/trailer.py19 KB715bf20f58fbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/app.py35 KB88267603c127upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/blender_gpu.py1.1 KB6279f1bedad9upstream ↗
# synced from github.com/apekshik/anyshot agent/blender_gpu.py """Every Blender the agent starts renders Cycles on the GPU, even with --factory-startup and no GPU prefs script. On the fal worker `blender` is a wrapper that runs this first (`blender.real --python blender_gpu.py "$@"`): headless Blender otherwise renders on the CPU, which an agent forgot once and lost most of a two-hour job to.""" import bpy from bpy.app.handlers import persistent def _devices(): try: prefs = bpy.context.preferences.addons["cycles"].preferences except (KeyError, AttributeError): return for kind in ("OPTIX", "CUDA"): try: prefs.compute_device_type = kind prefs.get_devices() except TypeError: continue if any(d.type == kind for d in prefs.devices): for d in prefs.devices: d.use = d.type == kind return @persistent def _gpu_scenes(*_): for sc in bpy.data.scenes: sc.cycles.device = "GPU" _devices() _gpu_scenes() bpy.app.handlers.load_post.append(_gpu_scenes) bpy.app.handlers.render_init.append(_gpu_scenes)builder/client.py4.3 KB42bb423d1ea0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/CRAFT.md3.8 KB1ce00e10dad7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/gallery.py17 KB6c71a3b3d91eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/PROMPT.md7.9 KB2496cfacafb6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/scene_builder.py24 KB40ff760645baupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
builder/sources.py9.9 KB8235a55a433aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
camera/camrig.py23 KB4564e727a1f6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
camera/examples/ramen_shots.json1.5 KB5e4cc09fbf46upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
camera/examples/repair_shots.json1.4 KB6272d73ede97upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
camera/shoot.py7.9 KB796033ffd7b6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/endpoint-map.html20 KB7db9bffd12e3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/ENDPOINT.md8.6 KB4289f8dd3134upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/bakery.jpg68 KB4ab5ca875b04upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/bakery_reference.jpg66 KB10a87ab936e0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/hk.jpg41 KB7734536ea5b6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/mc.jpg46 KB97207e792f1dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/ramen.jpg34 KBcaf2e187d1f5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/repair.jpg22 KB09c0dec46e7dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/media/strip.jpg68 KB30d4d828100cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/app.py22 KBd4f72c69dd05upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/assemble.py14 KBf04075ddc720upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/blockout.py6.2 KB9a91e42fbb4aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/client.py4.6 KB71831b31fb5bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/floorplan.py5.3 KB86cd64c8966eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/planner.py7.1 KB1895d60b358aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/props.py9.3 KB80abd15313a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/refine.py7.2 KBa06b57d6c2b5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/refine_blender.py14 KB70a49a315599upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/schema.py4.4 KB48e47ae02431upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
endpoint/stages.py1.2 KBc00afe04df64upstream ↗
"""Stage events: one JSON object per line on stdout, prefixed so clients can pick them out of the fal queue logs.""" import json import sys import time PREFIX = "ANYSCENE_STAGE " class Stages: def __init__(self, request_id: str = ""): self.request_id = request_id self.t0 = time.time() self.current = None def emit(self, stage: str, status: str = "running", message: str = "", progress: float | None = None, preview_url: str | None = None, **extra): event = {"stage": stage, "status": status, "message": message, "elapsed_s": round(time.time() - self.t0, 1)} if progress is not None: event["progress"] = round(progress, 3) if preview_url: event["preview_url"] = preview_url event.update(extra) print(PREFIX + json.dumps(event), flush=True) sys.stdout.flush() self.current = stage def start(self, stage, message=""): self.emit(stage, "running", message) def done(self, stage, message="", **extra): self.emit(stage, "done", message, **extra) def fail(self, message): self.emit(self.current or "unknown", "failed", message)endpoint/test/plan_sample.json4.3 KB0cbcde9f3bddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
farm/.bundles.json304 Ba7403cd3c379upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
farm/app.py11 KB51daea0f693cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
farm/farm.py7.8 KBab598457cbd4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
farm/jobs/hk-dai-pai-dong.json65 B6156b5840de5upstream ↗
{ "blend": "hk.blend", "scripts": ["scripts/gpu_prefs.py"] }farm/jobs/mission-control-1970s.json144 B9cff564f43f2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
farm/pack.py1.8 KB0dae6db76198upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/concepts.sh2.9 KB2d25cb59e211upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/fal.mjs9.5 KB8a6ea546416fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/gen3d.sh459 Bcde8765fb6d0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/models.mjs540 Bc6e74464ba36upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/props.sh2.4 KBa8cfa67b82f7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/samples.sh3.2 KB28eb341598f9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
generate/schema.mjs1.2 KB69e39238b6bcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
LICENSE1.0 KB197f55410b49upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
package-lock.json21 KBd87fabd7f215upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
package.json674 B0cc6b5352ee7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/build_compare_page.mjs11 KBf4917682a086upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/build_scenes_page.mjs31 KBded8363808efupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/env.sh1.2 KB79b7fd49df41upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/gpu_run.sh1022 Ba86c987f2bf6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/grounding.py5.7 KBc41d5a2652d5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/lan_gallery.py5.9 KBd5c5f9685b9fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/r2.mjs3.9 KB77d6c7fec93dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
pipeline/render_compare.py8.0 KB6f94fc338129upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/assets.json22 KBc7cae3696cf2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/brief.md3.7 KB60e5edba9a91upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/NOTES.md11 KB9c700c9845d7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/picks.json10 KB7db1776ded59upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/bake_steam.py1.5 KB88896769eea9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/blend.sh649 B7ba629cc40a5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/bts.py13 KB67bb5b45dae1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/bts_clay_redo.sh715 Bf856c1ff54e2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/bts_encode.sh1.3 KBf5b114578fe5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/build.py5.8 KBbcded739831cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/check_path.py1.9 KB0bf38204ce37upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/compare.py7.9 KB4853f3e33e84upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/compare.sh1.5 KBf1ae0d268f39upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/dress.py23 KB5e5d690c8104upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/emitters.py1.8 KBd56e3e3c2fcbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/flame_try.py1.6 KB4835e04336a8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/fx.py7.9 KB9295ce51daf9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/gen.mjs3.1 KB0a2c9aeeef1cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/gpu_prefs.py508 B855f62858438upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/grade.sh1.4 KB55d34a6cd53fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/grime.py6.0 KBc30afd14e470upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/ground_stills.py2.0 KB3b92d9d86c09upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/lib.py17 KB5cc580eaf763upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/lights.py11 KBa291c89a5561upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/manifest.py19 KB7a1611d6c255upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/pick.mjs507 Bdb6bb58c581bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/post.py1.5 KBaa8a025536c2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/prep.py12 KB181a1903afc9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/reel.sh1.0 KBf251a47d8e28upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/render_bts.sh2.5 KBaa528c0b3c0aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/render_finals.sh1.2 KB18df539e3897upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/review.sh1.0 KBc1f2a70dd004upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/sheet.py1.6 KBabb7b695ca5aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/shell.py30 KBa56da1977914upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/shoot.sh477 Bbffa69a50e71upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/steam.py7.6 KBa85cc1cbc08eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/stills.py1.1 KB031618a692eaupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/scripts/validate.py7.0 KB8c24f2724ff9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/hk-dai-pai-dong/shots.json6.3 KB1b0df1ce39f5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/assets.json22 KBab3d39aaefbeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/brief.md3.1 KB5083d9395b7dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/NOTES.md9.8 KB2a939dbabe3fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/picks.json8.9 KB4e0d13568a8eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/bts.py13 KB1fd1f2378f28upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/bts_render.sh1.7 KB6143e5efabbdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/build.py1.8 KB2c66a3996db0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/check_path.py1.9 KB12504549faa1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/compare.py7.9 KB8b91487b4bcdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/compare_batch.sh1003 Bad2e363c818bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/dress.py20 KBcfdac12445a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/fixes.py9.1 KB9ab224104974upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/fixscreens.py6.4 KBd1f523c84d3dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/gen3d.mjs1.5 KB2eb15046f5c6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/gpu_prefs.py430 B806545e7cc22upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/grade.sh1003 B97b3bcb52da0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/grime.py11 KBa4da73fc4338upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/haze.py4.5 KB70042ff4e53dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/layout.py2.0 KB7f9ea4c2e3a1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/lib.py14 KB38b6bd8fe28dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/lights.py5.1 KB4b9cf8f1350cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/lowcams.py1.2 KB9337e269be63upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/nocurl.py333 Bad5032722719upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/post.py2.7 KBf9fae59749eaupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/prep.py5.4 KB767681dd9d48upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/preview.py1.8 KB08f1bcba0cbdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/prevsheet.sh771 B333e0091ca72upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/refs.mjs1.9 KBe6f66f881937upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/refsheet.sh870 Bc03837ffaaeaupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/render_finals.sh1.4 KBe43415319c12upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/screens.py17 KB214ac2ec52faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/sheet.sh1.1 KB2ec52d120e0cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/shell.py12 KBe0b35305813aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/shoot.sh550 B6896c1dcd438upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/scripts/validate.py9.9 KB051080e27723upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/mission-control-1970s/shots.json6.6 KBbea9e3d7a91bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/assets.json33 KB2a61585f403aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/nb_swap.json760 B5014c1c08f68upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/NOTES.md7.8 KBfdb85d72de86upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/picks.json12 KB244a7fa786d1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/bts.py16 KBa6300fb37fdfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/bts_render.sh1.7 KB92da2e8f066eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/build_shell.py15 KB8661875f62e2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/cameras.py3.5 KB17a04847af51upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/check_path.py1.9 KB09db36f506aeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/compare.py8.2 KB358791f19734upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/compare_ready.sh536 B7d1c0f5e176eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/debrand.py10.0 KB2a0b9bc23647upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/dress.py12 KBf22d4ea7900dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/dress2.py61 KB54e3ffa39b50upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/fixes.py11 KBd07b010e3914upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/gen3d.mjs1.5 KB327417211ff9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/gpu_prefs.py430 B806545e7cc22upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/grade.sh987 B71c0cbb2e428upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/grime.py14 KB3854959bb363upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/lib.py14 KB38b6bd8fe28dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/lights.py9.5 KBa043e8ddfa09upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/prep.py4.5 KBe9d43e17a5e9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/preview.py2.2 KB4ff88293dc72upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/rebuild.sh1.1 KB295f96d66209upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/rebuild2.sh1.9 KB4264a07258adupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/refs.mjs1.5 KB2c100b326f8aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/render_clip.sh748 Bc93694568498upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/render_finals.sh1.4 KB196ff5d987f5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/render_frames.py1.3 KB713a1733db72upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/render_round2.sh2.1 KBa9e450651e3dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/round2_manifest.mjs9.5 KB4ca8a3a90d39upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/sheet.sh1.4 KBc5e4053c791bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/shoot.sh508 Bfe086f87d4a3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/validate.py4.7 KBc498c4c47631upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/scripts/views.py2.9 KBe596c45494afupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-electronics-repair/shots.json8.7 KB09a987479af7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/assets.json30 KB0a1c86bb80e8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/NOTES.md7.7 KB0d77a8727b7bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/picks.json10 KB3b6d21599adeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/round2.json13 KBf67a44aaa4ffupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/add_nb.mjs4.2 KB8fa8a63c4b4bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/bake_steam.py1.5 KB88896769eea9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/bts.py15 KB1b6df84f1856upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/build.py8.3 KB11b611df85f0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/camcheck.py2.5 KBc5ddbb93576cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/camera.py2.2 KB4df6f7f7458bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/clutter.py23 KBda865ccfd14eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/compare.py8.2 KB322696f2091dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/compare_rest.sh978 B31c7bb6dc763upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/dress.py18 KB8d3d4d6e1db9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/emitters.py1.8 KBd56e3e3c2fcbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/exterior.py9.7 KB88b765c8ebabupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/finals_gen.sh1.6 KB669880932b29upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/finals_r2.sh548 B8d99d66f24c9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/finish.sh875 B9b2f64802794upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/finish_r2.sh1.5 KB1db549de4bbfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/gen.mjs3.0 KB51cd531939deupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/gpu_prefs.py508 B855f62858438upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/grime.py8.1 KB340f1cdcd475upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/heroes_r2.sh517 B5bc5a35438baupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/lib.py17 KB86c49d178739upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/lighting.py4.9 KBd914f0014139upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/pick.mjs508 Bd98e3df85ae1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/post.py1.3 KB49e1caa770b5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/prep.py11 KB609244e273f8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/process_r2.sh914 B3ed03b90b03cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/r2gen.py12 KB0064d7a41463upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/rebuild.sh872 B2b66b952b914upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/render.py2.2 KBbaa5a518bdceupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/render_bts.sh2.6 KBbc49a4cb7b6bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/render_clip.sh616 B113e761a7a92upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/review.sh994 B09e391a4cca3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/shell.py14 KB1db096afeb8fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/shots_r2.sh990 Bb8ba736ff2dfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/steam.py7.6 KBa85cc1cbc08eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/scripts/validate.py3.6 KB4a65aa3b936fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
scenes/tokyo-ramen-shop/shots.json10 KBe41322390f30upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/build_level.py81 KB03dc2d04ff43upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/CHANGELOG.md11 KBa150fa6e6672upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/export.sh1.7 KBe8e814d7b39dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/export_blend.py77 KB680f5e684857upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/FX_AGENT.md6.9 KBaa0e595018ecupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_export.py37 KB5335d032482fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_pack.py33 KB74d14d19478aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Drips.json694 B372a30714138upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_DustMotes.json633 B25f0a2bf7242upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Fire.json5.1 KB0b3f18fcb946upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_FountainJet.json769 B6cf641829330upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Rain.json708 B6d1b850124e0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Smoke.json686 B6fd6c6f43714upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Sparks.json1.1 KB176d6d6bb75dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fx_recipes/NS_Steam.json702 B37501e58c53cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/fxlib.py1.4 KBb2c744ce4c28upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/NIAGARA.md53 KB72bd5cb705faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/perf.ps12.9 KB75216fc4b78fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/render_mrq.ps11.7 KB322b5b43f8d1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/side_by_side.py2.2 KB21ba7e3909b4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/templates.json6.1 KBf2472655fcdeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/templates_report.json18 KB4258677bc65fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal/TOGGLE.md11 KBccde40090d9aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/build_level.py33 KB66897bb9bfe1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/export.sh1.3 KB92e1f3fa66fbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/export_blend.py43 KB89ff833e7877upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/perf.ps12.7 KBa601daf811a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/render_mrq.ps11.1 KB841229aee3a6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/_orig/side_by_side.py1.8 KBe55eddd506c3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/build_level.py34 KBe9e810556ee0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/export.sh1.3 KB92e1f3fa66fbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/export_blend.py43 KBafb8c1c7d910upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/NIAGARA.md53 KB72bd5cb705faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/perf.ps12.9 KB4aeb4b4c6b3aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/render_mrq.ps11.2 KBcaa346af4d88upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/side_by_side.py2.2 KBeba72156e3d4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_bakery/TOGGLE.md11 KBccde40090d9aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/build_level.py57 KB7668193409abupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/export.sh1.5 KB990ac5293ac2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/export_blend.py57 KB41fb36bed0f9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/MERGE_BACK.patch55 KB7868bc0d399cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/NIAGARA.md53 KB72bd5cb705faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/perf.ps12.8 KB35d417afae42upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/render_cycles_refs.py2.8 KB38c9425e6d4aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/render_mrq.ps11.5 KBf7b961aee9edupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/run_chamber.sh2.1 KBab3a46d1b4daupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/scene_defaults.patch4.6 KBb4de3e24b25eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/side_by_side.py2.2 KB00cb893a779fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_caustic/TOGGLE.md11 KBccde40090d9aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/bake_cams.py3.5 KB90c0787421d5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/h1_walk_in_to_wok.json303 KB1205f1e97743upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/h2_walk_tables_neon.json252 KBacd347c89170upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/s1_street_crane.json74 KBfd6c9f228570upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/s2_teapot_wok_rack.json88 KB9f4400a9585dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/s3_wok_push.json59 KBd74d00622dfcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/cams/s4_neon_tilt.json75 KBaa3880c15aceupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/check_match.py3.3 KBccd2ddf30f62upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/demo_cut.py9.6 KBf7588d620d9cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/make_seqs.py8.5 KB91db9a339d2aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_demo/render.sh1.4 KB3f969714a9c5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/app.py37 KB8e8c8753c6d6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/build_level.py33 KB7b0892dff205upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/client.py2.9 KB386186328e0fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/export_blend.py60 KB9bbd45e684a5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal/side_by_side.py1.8 KBe55eddd506c3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/app.py22 KB79ca3fe38b45upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/build_level.py33 KBd43da05117b5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/mrq_config.py2.8 KBebe79abdbb86upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/render_mrq.ps11.1 KB841229aee3a6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/smoke_shot.py2.2 KBef39acfc2ee4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/template/AnySceneUE.uproject364 B01b7dc2ad8a0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/template/Config/DefaultEngine.ini1.1 KB8f5848d1288bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/template/Config/DefaultGame.ini71 Bb33dbf3c1ebeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/TOGGLE.md11 KBccde40090d9aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/tools/call.py1.6 KB86591ad23ff4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/tools/fetch.py1.1 KB9ef22c13088bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fal_ue/tools/upload_export.py971 B3055094e7031upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/build_level.py40 KB176a9e9eb9d4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/CHANGES.md6.3 KB6e4521fb88e6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/export_blend.py43 KB89ff833e7877upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/FX_AGENT.md6.9 KBaa0e595018ecupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_export.py37 KB5335d032482fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_pack.py33 KB74d14d19478aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Drips.json694 B372a30714138upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_DustMotes.json633 B25f0a2bf7242upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Fire.json5.1 KB0b3f18fcb946upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_FountainJet.json769 B6cf641829330upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Rain.json708 B6d1b850124e0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Smoke.json686 B6fd6c6f43714upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Sparks.json1.1 KB176d6d6bb75dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fx_recipes/NS_Steam.json702 B37501e58c53cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/fxlib.py1.4 KBb2c744ce4c28upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/NIAGARA.md53 KB72bd5cb705faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/ORIG.md599 Bca9cfa978be3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/perf.ps12.7 KBa601daf811a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/render_mrq.ps11.1 KB841229aee3a6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/side_by_side.py1.8 KBe55eddd506c3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/templates.json6.1 KBf2472655fcdeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_fx/templates_report.json18 KB4258677bc65fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/build_level.py33 KB66897bb9bfe1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/export.sh1.3 KB92e1f3fa66fbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/export_blend.py43 KB89ff833e7877upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/perf.ps12.7 KBa601daf811a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/render_mrq.ps11.1 KB841229aee3a6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/_orig/side_by_side.py1.8 KBe55eddd506c3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/agx_view.py745 B870bf9e98dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/build_level.py34 KB1f012ed7e5bbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/csv_summary.sh1015 B1a3f187328c0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/export.sh1.6 KBe626c78add6cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/export_blend.py45 KB64ecb453b1daupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/FIXLOG.md8.1 KBf620a67210f7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/make_copy.py641 B0a0063e9b916upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/NIAGARA.md53 KB72bd5cb705faupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/perf.ps12.8 KBcf962b294c57upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/ramen_fixes.patch13 KB52ed4b554602upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/render_mrq.ps11.2 KBcaa346af4d88upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/render_shots.py9.1 KB0d38457918ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/side_by_side.py1.8 KBe55eddd506c3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
unreal_ramen/TOGGLE.md11 KBccde40090d9aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/.env.example1.6 KB7213c8bf6762upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/.gitignore387 Bbc93f3baf340upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/.vercelignore118 B03bf369d62efupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/AGENTS.md679 Baf3b7c84114bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/app/api/fal/proxy/route.ts3.6 KBabd7eb385b39upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/app/api/me/route.ts852 B8f9e6d385193upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/app/globals.css49 KBeac7ebd27866upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/app/layout.tsx749 B07ee57061ed6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/app/page.tsx173 Bb564ae38f1d3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/BuilderInputs.tsx5.4 KBa4dceae012f3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/BuilderResult.tsx4.6 KB5699b367c940upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/BuilderSchedule.tsx4.2 KB6f9885161c26upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/BuilderStudio.tsx19 KBd7e879c97b8cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/BuilderViewport.tsx11 KB635b9762b522upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Compare.tsx2.0 KB14bf1da679b9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/CriticPanel.tsx4.9 KB9bab599375e3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Dials.tsx3.2 KB16eae55d2e63upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Fade.tsx1.1 KB8bef5d6490baupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Gallery.tsx10 KBe7ba8b35a780upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/InfoTip.tsx3.1 KB256f1c9b3a1bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Inputs.tsx4.1 KB48f1f308fe21upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Lightbox.tsx3.5 KB8d611b8901deupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/LiveMap.tsx4.3 KB942ab68c1fbdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Pano360.tsx4.2 KB3c182fa45d73upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/PlanPanel.tsx6.2 KB69a492c51bd1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/ProductionLog.tsx4.1 KB0416b73e5a86upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/ProductSwitch.tsx942 B2a68f2e03fd8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/PropShelf.tsx5.2 KBda16d314a670upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Result.tsx3.4 KB4466b235a768upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/SamplePicker.tsx4.4 KBacab4b0b040bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/SceneView.tsx5.2 KBf8979f3cc19fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Shell.tsx1.3 KBb375d07d86dfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/ShotsOrder.tsx6.8 KB248daeaa30d0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Stages.tsx3.2 KB89bf90eba1f4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Stamp.tsx428 B40db81c9096eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Studio.tsx12 KB6ed393718c58upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Viewer3D.tsx7.8 KB70029a25bd83upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/Viewport.tsx11 KBef550e886be5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/components/WhoAmI.tsx3.9 KB07c4fe2bf638upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/apps.ts1.9 KB9e51e500f449upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/builder.ts10 KB20fd156966e8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/gallery.ts6.0 KB2bc3c83bca77upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/guard.ts5.6 KBe64aef59fe2dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/run.ts8.5 KB527d63de31bcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/samples.ts1.4 KBfed008342bb0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/sources.ts20 KBba3e1c832538upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/three-util.ts5.8 KB36741e80fd0bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/lib/types.ts5.3 KB75052e3a1cccupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/next.config.ts502 B5b316aa5420dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/package-lock.json51 KB0ee1078d1e2cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/package.json1.0 KB41a931a8b7ddupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/apothecary.jpg316 KB71009d460daeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/apothecary_thumb.jpg127 KB51e11af241feupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/arcade.jpg224 KBee0881b808d7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/arcade_thumb.jpg90 KBdce9f9ef1599upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/cabin.jpg230 KB233a4a583551upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/cabin_thumb.jpg88 KBae0448f82450upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/cargo.jpg244 KB350deeaca2e2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/cargo_thumb.jpg91 KB02e66fb4c5afupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/diner.jpg204 KBc9c793fcf2b4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/diner_thumb.jpg85 KBec0ca30e2951upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/forge.jpg271 KB45f998b9d434upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/forge_thumb.jpg103 KBd59ed5d4b282upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/garage.jpg276 KBe081350ccbd9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/garage_thumb.jpg108 KB6a3ef896e034upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/greenhouse.jpg427 KB6ced06ea545aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/greenhouse_thumb.jpg159 KBa6cc529fb96cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/jazz.jpg178 KB4827fb0dc45bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/jazz_thumb.jpg71 KB8eb1638cc8d1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/library.jpg252 KB7e8073d89dc3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/library_thumb.jpg94 KB85c0434b37f3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/noir.jpg161 KB3990441f9e93upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/noir_thumb.jpg62 KB178dab930cfbupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/subway.jpg196 KB1b827131d977upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/public/samples/subway_thumb.jpg74 KB6c82f4d20feaupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/01-idle.png190 KB23c8243845d2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/02-planning.png198 KB8d85699c1208upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/03-plan-ready-greybox.png474 KB9565703f090aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/04-floor-plan.png286 KB69ad1774c8bcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/05-compare-greybox.png701 KB036a2be65029upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/06-build-references.png365 KB1d7bf58aab0aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/07-build-meshes-prop-map.png420 KB62b5c2de9cdcupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/08-live-layout.png619 KBff5c24042635upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/09-prop-3d.png505 KBee004a7a9189upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/10-wrapped-result.png543 KB6bce4ed385f9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/11-compare-final.png791 KBe3c5562f65d5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/12-360-plate.png422 KBc03de21c7d2bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/13-360-set-model.png477 KB2b401bb28c2cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/14-prop-3d-scene-light.png510 KB3a6e65f68fc7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/15-mobile.png180 KB7b1552c5fff5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-01-idle.png193 KB546249a0a6baupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-02-plan-milestone.png300 KBed820c123501upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-03-props-dressing.png570 KB44a261c76b37upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-04-critic-round-1.png626 KB637853d2b795upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-05-contact-sheet-round-2.png469 KBfe3735384801upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-06-log-notes-filter.png487 KB6405d7eb55cdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-07-wrapped-stills.png491 KB24dd4cb665f7upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-08-compare.png736 KBe9039d79998aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-09-360-plate.png389 KB168c6b65cce4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-10-360-set-model.png435 KB5fae401a3708upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-11-mobile.png178 KB28ef6932538aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/screenshots/builder-12-pipeline-toggle.png112 KB9f974d542029upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/import-builder-run.mjs1.6 KB8da09e18498aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/make-builder-fixture.mjs12 KB0a8b3649b6a2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/make-placeholders.mjs10 KBc6b4d5a4a248upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/screenshots-builder.mjs3.0 KB074f16ee4c72upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/screenshots.mjs3.6 KB51e4b15084f1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/sync-fixtures.mjs2.8 KB6c4a58e03c5bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/scripts/write-env-local.mjs1.2 KB03917041c0e3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
web/tsconfig.json714 B469c29726bbdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/apekshik-anyscene.md




