PAL Layout (Lenscowboy)
Stop guessing depth — PAL Layout gives you real depth maps from an actual 3D scene
- model_3d
- beauty pass
- alpha pass
- depth pass
- normal pass
- id matte pass
- scene json
- camera json
- frame start
- frame end
- sequence json
Every ControlNet depth workflow you've seen starts by estimating depth from a flat image with Marigold or MiDaS. The estimate is good - and it's still an estimate. PAL Layout (PAL is Lenscowboy's 3D previsualization tool) takes the other route: you actually build the shot. Place the camera, drop in proxy geometry, set the lighting, and the node hands you a depth map rendered from real geometry. Same for normals. No preprocessor, no hallucinated geometry - just the exact camera you chose, as pixels.
That's the whole reason to reach for it. Depth-conditioned ControlNet already gives you strong spatial control; what it inherits is the quality of your condition. A rendered pass beats a monocular estimate every time, and if you're doing shot design - character placement, camera moves, consistent framing across frames - it beats alt-tabbing into Blender too.
Honest framing up front: this is a young, small pack. v1.1.x, launched spring 2026, almost no community footprint yet. You're an early adopter, and the README is candid that some things (animated sequence renders, camera data import) were still landing after v1.0. The author is active and responsive on Discord, which helps when you hit the rough edges.
How it works
It's a hybrid of a browser app and a Python node. The frontend embeds Lenscowboy's full Three.js viewport as an iframe (served from app.lenscowboy.com by default). You get orbit/pan/zoom, camera presets (eye level, low angle, overhead, rear, OTS, POV), WASD fly mode, proxy geometry for humans, vehicles, buildings and terrain, plus a shot list and timeline. When you render, passes flow back over a postMessage bridge as base64, get stashed in a hidden _pal_scene_state widget, and are decoded into IMAGE tensors at execute() time.
There's a second render path: flip use_local_renderer on and rendering happens in-process with pygfx/wgpu - no iframe round-trip, no base64 cap. Everything else is dependency-light: the pack's requirements.txt declares nothing beyond ComfyUI's own torch/numpy/PIL, because the Three.js viewport runs in your browser, not the server.
The inputs that matter
Everything is optional - the node works fully offline with no API key.
model_3d,GLB,OBJ,glb_path- your 3D assets.model_3dtakes a FILE_3D socket matching ComfyUI's native Load3D output exactly, so an image-to-3D result (Hunyuan3D, TRELLIS) wires straight in. GLB/OBJ also accept FILE_3D/MESH/STRING, or type a file path intoglb_path. The viewport supports drag-and-drop GLB import too.camera_preset- a string, defaulteye_level; pick a preset or type your own.frame_start/frame_end- timeline range (defaults 1–24). v1.1 added sequence rendering, so you can hand an IMAGE batch of frames to KSampler or Video Combine.render_width/render_height- 64–2048, step 64, default 512. The free tier caps at 512 regardless.lc_api_key,lc_project_id,lc_shot_id- the optional Lenscowboy platform connection for project/shot data.use_local_renderer- the pygfx path.
Outputs - and the catch
Ten outputs: beauty, alpha, depth, normal and id-matte IMAGE passes, scene/camera/sequence JSON strings, and the frame range. The beauty pass covers preview, reference and img2img. Depth and normals feed the corresponding ControlNet models with the same wiring as any preprocessed map.
Here's the trap nobody spells out: depth, normal, alpha and id-matte are gated behind a paid Lenscowboy plan. The source literally sets has_multipass = plan != "free" and fills those outputs with blank frames on the free tier. Free users get the 512px beauty pass and nothing else. So the feature most people want this node for - real depth/normals into ControlNet - needs an API key and a subscription. Generate a key at app.lenscowboy.com/settings (they expire after 365 days) and check your plan before you go chasing a black depth map. Keys unlock features per-plan, and revoking is instant.
Install
ComfyUI Manager, search "PAL Layout". Or:
cd ComfyUI/custom_nodes
git clone https://github.com/Lenscowboy/comfyui-lenscowboy-pal.git
Restart ComfyUI. No model downloads, no heavy dependencies. Want the local renderer? Its extras install separately so the node still loads without them:
cd ComfyUI/custom_nodes/comfyui-lenscowboy-pal/pal_renderer
pip install -r requirements.txt
That graceful fallback to iframe mode is deliberate - the base install is genuinely zero-friction.
Troubleshooting
- Queue does nothing. The node blocks Queue Prompt until passes are rendered (via
beforeQueuePrompt). Open the viewport, arrange, render, save, then queue. - Blank depth/normals. Almost always the free-tier gate above, not a bug.
- FBX cameras don't import. At launch the node only recognized geometry, not cameras baked into FBX exports; the author was mid-fix in April 2026. If your pipeline is C4D/Blender cameras, test with a simple FBX first.
- It always re-runs. The node returns NaN from
IS_CHANGED, so ComfyUI executes it on every queue. Fine for an interactive tool, mildly annoying in batches.
Inputs (16)
| Name | Type | Default | Description |
|---|---|---|---|
| lc_api_keyopt | STRING | — | |
| lc_project_idopt | STRING | — | |
| lc_shot_idopt | STRING | — | |
| glb_pathopt | STRING | — | |
| GLBopt | COMBO | 4 options: FILE_3D_GLB, FILE_3D, MESH, STRING | |
| OBJopt | COMBO | 4 options: FILE_3D_OBJ, FILE_3D, MESH, STRING | |
| model_3dopt | FILE_3D | — | |
| promptopt | STRING | — | |
| camera_presetopt | STRING | eye_level | — |
| frame_startopt | INT | 10–9999 | — |
| frame_endopt | INT | 241–9999 | — |
| render_widthopt | INT | 51264–2048 | — |
| render_heightopt | INT | 51264–2048 | — |
| scene_json_inopt | STRING | — | |
| use_local_rendereropt | BOOLEAN | false | — |
| _pal_scene_stateopt | STRING | {} | — |
Outputs (10)
| Name | Type | Description |
|---|---|---|
| beauty pass | IMAGE | — |
| alpha pass | IMAGE | — |
| depth pass | IMAGE | — |
| normal pass | IMAGE | — |
| id matte pass | IMAGE | — |
| scene json | STRING | — |
| camera json | STRING | — |
| frame start | INT | — |
| frame end | INT | — |
| sequence json | STRING | — |