# Product Photo to 3D Model with Real-World Scale (`toolsheder/photo-to-3d`) Actor

Turns product photos into web-ready GLB models scaled to the real dimensions you give, oriented front-forward, with the pivot on the floor. Ready to drop into Shopify, WooCommerce, Wix or any model-viewer page.

- **URL**: https://apify.com/toolsheder/photo-to-3d.md
- **Developed by:** [Kostas Skutulas](https://apify.com/toolsheder) (community)
- **Categories:** E-commerce, AI, Automation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $400.00 / 1,000 model delivereds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Product Photos to 3D Models at Real-World Scale

Turn product photos into GLB models scaled to your exact millimetre dimensions, with the pivot on the floor, ready for Shopify, WooCommerce and Wix.

### Introduction

AI can turn a product photo into a 3D mesh in about a minute. What it cannot do is get the size right. Image-to-3D generators normalise their output to roughly a one cubic metre volume and ignore the object's real proportions, so a dining chair comes back the same size as a three-seat sofa. They also drop the origin at the centre of the object rather than at its base.

Both problems have the same consequence: augmented reality stops working. A model with the origin at its centre sinks halfway into the shopper's floor. A model at the wrong scale stands next to a real door frame and is obviously, uselessly wrong. Since "see it in your room" is the entire reason to put 3D on a product page, a model with the wrong scale is worse than no model at all.

This actor fixes both. You already know your products' dimensions, because they are printed on the product page. This actor treats those dimensions as ground truth and forces the generated mesh to match them: exact width, height and depth in millimetres, origin on the floor, centred horizontally, upright.

What comes out is a GLB you can upload straight into Shopify's 3D media slot, where the built-in viewer and AR work with no app at all. The same file works with the free viewer plugins on WooCommerce and Wix, and with `model-viewer` on any custom site. iOS AR Quick Look is served from that same corrected file, so what a shopper sees on an iPhone is the model you approved.

The output follows the conventions the platforms actually document: glTF's metre unit and +Y up, Shopify's requirement that the origin sits at the product's base, and Google's Scene Viewer guidance on triangle and file size. The defaults deliberately favour AR working everywhere over the smallest possible file.

### Tutorial

#### 1. Get a provider key

You bring your own Meshy or Tripo API key. The generated models are then licensed to you directly by the provider, with nobody in between, and you pay generation costs at their list price rather than a marked-up rate.

A paid provider tier is normally required if you intend to sell or redistribute the models. Meshy's free tier licenses output under CC BY 4.0, which requires attribution; its paid tiers grant ownership outright. Tripo grants paid users broad commercial rights, while free accounts assign the rights to Tripo. Read your provider's terms before putting the results on a live storefront.

#### 2. Collect dimensions

For each product you need the real width, height and depth in millimetres. This is the only input the actor cannot guess for you, and it is the input that makes the result worth having.

#### 3. Give it two or three photos

One photo makes the generator invent the back of the object. Two or three angles produce a noticeably better mesh. The first URL you give is treated as the front view.

#### 4. Run and check the preview

Each row of the output table shows the size the model actually measures after processing, next to the size you asked for. If they match, the model is right.

#### 5. Upload

Download the GLB from the link in the output and add it to the product's media. On Shopify that is all: the 3D viewer and mobile AR switch on by themselves.

### Pricing

This actor charges per event, and only for work that succeeded.

| Event | When it fires |
|---|---|
| `run-started` | Once per run, whatever it contains |
| `model-delivered` | Once per usable model, never for a failure |

A product that fails to generate costs you nothing here. Provider credits are billed separately by Meshy or Tripo against your own key, and the output table reports how many credits each model consumed so you can see the real per-model cost.

### Input and output

#### Input

```json
{
  "provider": "meshy",
  "providerApiKey": "your-provider-key",
  "scaleMode": "exact",
  "items": [
    {
      "id": "sofa-oslo-3s",
      "imageUrls": [
        "https://example.com/sofa-front.jpg",
        "https://example.com/sofa-side.jpg"
      ],
      "widthMm": 2100,
      "heightMm": 850,
      "depthMm": 950
    }
  ]
}
```

#### Output

```json
{
  "id": "sofa-oslo-3s",
  "status": "ok",
  "glbUrl": "https://api.apify.com/v2/key-value-stores/.../records/sofa-oslo-3s.glb",
  "requestedSizeMm": [2100, 850, 950],
  "measuredSizeMm": [2100, 850, 950],
  "triangles": 38412,
  "fileSizeBytes": 2841200,
  "yawApplied": 0,
  "distortion": 1.06,
  "dimensionMismatch": 1.06,
  "creditsConsumed": 30
}
```

`measuredSizeMm` is measured on the finished file, not assumed. `dimensionMismatch` is how far the generated mesh was from your dimensions before anything was done about it: 1.0 means they agreed. `distortion` is how much stretching was actually applied, which is always 1.0 in uniform mode. A high mismatch usually means the model came out facing the wrong way.

### Actor recommendations

**Set `yawDegrees` when a model faces the wrong way.** Nothing can work out which side of a sofa is the front from geometry alone, and this actor does not pretend otherwise. It will turn a model a quarter turn when that clearly fits your width and depth better, but a front-to-back flip needs one number from you. Look at the preview, add `"yawDegrees": 180`, run that product again.

**Leave Draco compression and WebP textures off unless you know you need them.** Both make the file smaller. Both also rely on glTF extensions that Google's Scene Viewer does not list among the two it supports, which means AR on Android can fail. The defaults choose working AR over a smaller download.

**Use `scaleMode: "uniform"` when your dimensions include something the mesh does not.** Exact mode stretches the mesh to hit your numbers, which is right when the numbers describe the same object the photos show. If your listed depth includes a headrest the photos do not, uniform mode keeps the generated proportions and fits the model inside your box instead.

**Show the model with the right tone mapping, or your own viewer will wash it out.** This one costs nothing and is worth more than any setting in this actor. Measured against the original photographs in CIELAB, `model-viewer` keeps 66% of a sofa's chroma under `tone-mapping="neutral"` and only 29% under `agx` — AgX is a filmic mapper and desaturates on purpose. Neutral is already the default in `model-viewer` 4.x, so the usual mistake is overriding it. Two traps come with it: values outside `aces`, `agx` and `neutral` silently fall back to Neutral rather than erroring, and exposure is quietly multiplied by 1.3 when Neutral runs against the `neutral` or `legacy` environment, so exposures are not comparable between tone mappers.

**Turn off dynamic render scaling if you use `auto-rotate`.** `model-viewer` drops to a quarter of the pixels when a frame takes over 60 ms and only restores full resolution once the scene stops moving. A rotating model never stops, so the page stays blurry permanently rather than just while dragging. One line, after the element is defined:

```js
customElements.get('model-viewer').minimumRenderScale = 1;
```

Its getter returns `undefined`, so check the `render-scale` event rather than reading the property back.

**Watch the mismatch column.** Anything above about 1.3 is telling you something is wrong with that product: bad dimensions, a bad mesh, or a wrong yaw. It is reported in both scale modes, because it describes the disagreement itself rather than what was done about it.

### FAQ and support

**Do I need a Shopify app to display these?**
No. Shopify supports GLB natively: upload it to the product's media and the 3D viewer plus mobile AR switch on with no app. WooCommerce and Wix have free viewer plugins already installed on hundreds of thousands of sites.

**Why is there no USDZ file for iOS?**
Because the one the generators hand back is not the model you approved. It comes at their own arbitrary scale with their own origin, and none of the corrections this actor makes are applied to it. Shipping that next to a correct GLB would put a wrongly sized model in front of every iPhone shopper. Shopify and `model-viewer` both derive iOS AR from the GLB instead, so the corrected file is what gets used.

**Why do I have to supply dimensions?**
Because that is the whole product. Both providers offer an "auto size" feature that estimates real-world height with AI vision, and an estimate is exactly what makes AR look plausible in isolation and wrong next to a real object.

**What if a photo produces a bad mesh?**
Shiny, transparent and very intricate products are the usual failures. You are not charged for a model that fails, and the output row explains what went wrong.

**Where are my files stored, and for how long?**
In a named Apify key-value store, which is kept indefinitely. Reuse the same storage name across runs to build one library. The download links stay valid as long as the record exists.

**Can I sell products using these models?**
That depends on your provider's terms and your tier there, not on this actor. See the tutorial above.

Report problems through the Issues tab on this actor.

# Actor input Schema

## `items` (type: `array`):

One entry per product. Give 2 or 3 photos from different angles, and the real dimensions in millimetres. The dimensions are what make the model the right size in AR, so they are not optional.

## `provider` (type: `string`):

Which image-to-3D service to call. Meshy also returns a USDZ file for iOS AR at no extra cost. Tripo can align the mesh to your front photo, which helps when models come out facing the wrong way.

## `providerApiKey` (type: `string`):

Your own Meshy or Tripo key. You bring your own, so the models are licensed to you directly by the provider with nobody in between. A paid provider tier is normally required if you intend to sell or redistribute the models.

## `scaleMode` (type: `string`):

Exact stretches the mesh to match your width, height and depth precisely, which is what you want when your dimensions are right. Uniform keeps the generated proportions and fits the model inside your dimensions instead.

## `maxTriangles` (type: `integer`):

Meshes above this are simplified once, on our side. 60000 is the measured sweet spot: within 0.35% of an unsimplified mesh in rendered pixels, well under AR limits. The provider's own remeshing stays off because it was measured to introduce holes.

## `maxTextureSize` (type: `string`):

Textures larger than this are resized. 2048 is the ceiling Shopify gives for mobile web.

## `textureFormat` (type: `string`):

JPEG is the safe default because it works everywhere, including Android AR. WebP makes smaller files but relies on a glTF extension that Google's Scene Viewer does not list as supported, so AR on Android may fail.

## `textureResolution` (type: `string`):

What to ask the provider for before the model is resized to your ceiling. 4k costs the same on most plans and downscales to a visibly sharper 2048 texture, which shows on fabric and wood.

## `dracoCompression` (type: `boolean`):

Shrinks geometry considerably, but Google's Scene Viewer does not list Draco among the glTF extensions it supports, so AR on Android may fail. Leave off unless you only need the web viewer.

## `pivotOnFloor` (type: `boolean`):

Generators put the origin at the centre of the object, which makes it sink into the floor in AR. Shopify requires the origin at the product's base.

## `recenterHorizontally` (type: `boolean`):

Centres the model on X and Z so it turns around itself rather than orbiting.

## `storeName` (type: `string`):

Models are saved to a named storage so the download links keep working. Reuse the same name across runs to keep one library. Unnamed storage is deleted after seven days, so this is not left to chance.

## `concurrency` (type: `integer`):

How many products to process in parallel. Leave empty to match your provider's queue limit. Raise the actor's memory if you raise this.

## Actor input object example

```json
{
  "items": [
    {
      "id": "sofa-oslo-3s",
      "imageUrls": [
        "https://example.com/sofa-front.jpg",
        "https://example.com/sofa-side.jpg"
      ],
      "widthMm": 2100,
      "heightMm": 850,
      "depthMm": 950
    }
  ],
  "provider": "meshy",
  "scaleMode": "exact",
  "maxTriangles": 60000,
  "maxTextureSize": "2048",
  "textureFormat": "jpeg",
  "textureResolution": "2k",
  "dracoCompression": false,
  "pivotOnFloor": true,
  "recenterHorizontally": true,
  "storeName": "photo-to-3d-models"
}
```

# Actor output Schema

## `models` (type: `string`):

One row per product: download link, requested versus measured size, triangles and warnings.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "items": [
        {
            "id": "sofa-oslo-3s",
            "imageUrls": [
                "https://example.com/sofa-front.jpg",
                "https://example.com/sofa-side.jpg"
            ],
            "widthMm": 2100,
            "heightMm": 850,
            "depthMm": 950
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("toolsheder/photo-to-3d").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "items": [{
            "id": "sofa-oslo-3s",
            "imageUrls": [
                "https://example.com/sofa-front.jpg",
                "https://example.com/sofa-side.jpg",
            ],
            "widthMm": 2100,
            "heightMm": 850,
            "depthMm": 950,
        }] }

# Run the Actor and wait for it to finish
run = client.actor("toolsheder/photo-to-3d").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "items": [
    {
      "id": "sofa-oslo-3s",
      "imageUrls": [
        "https://example.com/sofa-front.jpg",
        "https://example.com/sofa-side.jpg"
      ],
      "widthMm": 2100,
      "heightMm": 850,
      "depthMm": 950
    }
  ]
}' |
apify call toolsheder/photo-to-3d --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,toolsheder/photo-to-3d"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xGyHtITNGL1hwlKQ3/builds/iBNHcHgviKCDpa7DJ/openapi.json
