3D Product Placement (WepeNerd)
Drop a 3D product onto a photo with your mouse, not a prompt
- background_image
- obj_model
- diffuse_texture
- bump_map
- composite
- object_mask
If you've tried to get a specific product into a scene with prompts alone, you know the pain: the model gives you a close-enough bottle that's actually a different brand, sitting at a slightly wrong angle, lit like a museum exhibit. 3D Product Placement (WepeNerd) sidesteps all of it. You connect a normal background image, connect a real 3D model, and position it with your mouse in an interactive viewport - rotate, move, scale, move the light - and out comes a composite with the object placed exactly where you put it.
Be clear on what this is for: guide composites, not final renders. The object renders as an untextured clay model. That's the feature - you nail the placement, angle, and lighting for a client/art-director approval, then feed the composite into an img2img pass to get the photoreal version. This is the pack's flagship node and the one people actually search for.
How it works
Two renderers, one of which you'll normally never see. The viewport you interact with is a browser-side Three.js scene (the pack ships the Three.js modules and OBJLoader it needs). When you queue, the node tries to capture the hidden browser viewport as a PNG data URL and uses that as the composite, with a matching captured mask - so what you see is what you get. If the capture isn't available, it falls back to a server-side pyrender render using the same transform and lighting values. Placement values live in ordinary widgets, so your workflow saves and reloads exactly where the object was.
The interactions
| Action | Result | |---|---| | Left drag | Rotate object | | Shift + drag | Move object X/Y | | Mouse wheel | Scale object | | Right drag / Alt + drag | Adjust light direction | | Double-click | Reset placement |
The inputs that matter
- background_image - an IMAGE from a normal Load Image node. It's both the scene and the source of your output dimensions.
- obj_model - the
WN_OBJ3Doutput from Load OBJ (WepeNerd). Nothing else will plug in here. - x_offset / y_offset / z_offset, scale, rotate_x / rotate_y / rotate_z - the object transform. The viewport sets these as you drag, and you can fine-tune numerically.
- camera_zoom - orthographic camera zoom; 1.0 is default.
- light_yaw / light_pitch / light_intensity - the directional light.
- wireframe_overlay - dark wireframe over the clay object, great for exact alignment guides.
- opacity - object opacity in the final composite, useful for ghosted "here's where the product goes" diagrams.
- diffuse_texture and bump_map - optional IMAGE inputs; note that the server-side fallback render stays clay-only, so don't count on texturing in the fallback path.
What comes out
composite (IMAGE) - your background with the object composited over it. object_mask (MASK) - the rendered object's alpha, which you can feed into a mask-to-image or use for clean compositing elsewhere.
Installing it
Same pack, same install:
cd ComfyUI/custom_nodes
git clone https://github.com/WepeNerd/ComfyUI-WepeNerd.git
cd ComfyUI-WepeNerd
pip install -r requirements.txt
Or search WepeNerd in ComfyUI Manager, restart, and you'll find it under WepeNerd/3D. The heavy dependencies - trimesh, pyrender, PyOpenGL, pyglet - are all declared in requirements.txt.
Common gotchas
Two things to watch. First, the security note is real: this node registers a local HTTP route to serve OBJ previews, and the source carries a TODO to restrict it to safe folders. Fine for a local-only ComfyUI, but if you run with --listen or expose the server, keep model files in ComfyUI/input/3d/ or ComfyUI/models/3d/ and don't hand out absolute paths. Second, on headless servers the pyrender fallback needs an OpenGL context - the error message suggests EGL or OSMesa. And the evergreen one: the output is clay. If your object looks grey and you were expecting a product photo, that's the node working as designed - feed the composite into your img2img pass and let the model finish the render.
Inputs (20)
| Name | Type | Default | Description |
|---|---|---|---|
| background_image | IMAGE | — | |
| obj_model | WN_OBJ3D | — | |
| x_offset | FLOAT | 0-4096–4096 | — |
| y_offset | FLOAT | 0-4096–4096 | — |
| z_offset | FLOAT | 0-4096–4096 | — |
| scale | FLOAT | 3001–8192 | — |
| rotate_x | FLOAT | 0-360–360 | — |
| rotate_y | FLOAT | 0-360–360 | — |
| rotate_z | FLOAT | 0-360–360 | — |
| camera_zoom | FLOAT | 1.000.05–10 | — |
| light_yaw | FLOAT | 45-180–180 | — |
| light_pitch | FLOAT | 45-90–90 | — |
| light_intensity | FLOAT | 3.00–20 | — |
| wireframe_overlay | BOOLEAN | false | — |
| opacity | FLOAT | 1.000–1 | — |
| bump_scale | FLOAT | 0.030-1–1 | — |
| viewport_capture | STRING | — | |
| viewport_mask_capture | STRING | — | |
| diffuse_textureopt | IMAGE | — | |
| bump_mapopt | IMAGE | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| composite | IMAGE | — |
| object_mask | MASK | — |