Skip to content

3D models ​

slide.addModel3d() embeds a glTF binary (.glb) in the .pptx and places it on the slide as a 3D model, the kind PowerPoint's Insert > 3D Models makes. PowerPoint 2019 and later draw the model, and a viewer can drag to rotate it.

ts
import TsPptx from 'pptx-ts'

const pptx = new TsPptx()
pptx.addSlide().addModel3d({
  path: 'assets/engine.glb',
  preview: { path: 'assets/engine-render.png' },
  meterPerModelUnit: 1 / 240, // the model is 240 units across at its widest
  x: 1,
  y: 1,
  w: 6,
  h: 4,
})
await pptx.writeFile({ fileName: 'engine.pptx' })

data or path is required. Most models also need preview and meterPerModelUnit. The preview is what most readers draw, and the scale decides whether the camera frames the model. addModel3d returns the slide, so calls chain.

Options at a glance ​

OptionTypeDefaultEffect
datastringnoneThe .glb as base64, with or without a data: header
pathstringnoneA file path or URL of a .glb, read when the deck is written
preview{ path?: string; data?: string }a gray placeholder, with a warningThe picture drawn wherever the model is not
meterPerModelUnitnumber0.5Metres per model unit
cameraModel3dCameraPropsPowerPoint's camera for a 2 by 2 by 2 cubeThe viewpoint
x, yCoord0Top-left corner
w, hCoord4, 3Size
objectNamestring3D Model 1, 3D Model 2, ...Selection Pane name
altTextstringnoneAlt text
objectLockObjectLockPropsnoneLock flags on the model's frame

Supply a preview picture ​

A model is written inside an mc:AlternateContent element with two branches. The mc:Choice branch holds the model and names the namespace a reader must understand to use it. The mc:Fallback branch holds an ordinary picture. A reader that understands the namespace draws the mc:Choice branch and ignores the other. Any other reader skips mc:Choice and draws the picture in mc:Fallback. An OLE object uses the same two branches for its cover.

ReaderBranch it draws
PowerPoint 2019 and later: on screen, a slide exported as a picture, a PDF export, a printoutmc:Choice, the model
PowerPoint 2016 and earliermc:Fallback, the preview
any other application, such as LibreOfficemc:Fallback, the preview

The library has no 3D renderer, so pass the preview as preview, by path or as base64 data with a header:

ts
slide.addModel3d({ path: 'engine.glb', preview: { path: 'engine-render.png' } })
  • With no preview, a 32 by 32 gray PNG is embedded and model3d/preview-missing warns. PowerPoint 2019 and later draws the model on screen, in exports and in print, so the placeholder shows only where the fallback is read.
  • A preview.data with no base64 header warns preview-image/missing-base64-header, and the gray PNG is embedded instead.
  • The preview is stretched to the model's frame. Give it the frame's aspect ratio.
  • The preview is stored once, however many branches refer to it.
  • To make one, insert the model in PowerPoint, export the slide as a picture and crop it.

Set the scale ​

The 3D scene is measured in metres, and meterPerModelUnit sets how many metres one unit of the model is. When PowerPoint inserts a model, it reads the bounding box and scales the largest dimension to 1 metre. The library does not parse the .glb, so it cannot measure the box. It writes 0.5, which is right only for a model 2 units across.

Set meterPerModelUnit to 1 divided by the model's largest bounding-box dimension, in model units. Most exporters report that dimension. In the file it is the widest span between min and max on the POSITION accessors in the JSON chunk.

Largest dimension, in model unitsmeterPerModelUnit to setSize with that settingSize at the default 0.5
0.1101 m0.05 m
20.51 m1 m
201 / 201 m10 m
2401 / 2401 m120 m

The default camera sits 2.26 m from the centre of the scene. A 240-unit model left at 0.5 is 120 m across, so the camera is inside it and the slide shows a wall of shading.

  • The value is stored to six decimal places. 1 / 240 is written as 0.004167.
  • A value that is not a finite number above 0 throws model3d/invalid-scale, and so does a value below 0.0000005, which would be stored as 0.

Set the camera ​

camera overrides the viewpoint. A field you leave out keeps its default, and the defaults are the camera PowerPoint wrote for a 2 by 2 by 2 cube:

FieldTypeDefaultEffect
posModel3dPoint{ x: 0, y: 0, z: 2.2630334 }Eye position, in metres
lookAtModel3dPoint{ x: 0, y: 0, z: 0 }The point the camera aims at, in metres
upModel3dPoint{ x: 0, y: 1, z: 0 }The up direction, not necessarily a unit vector
fovnumber45Vertical field of view in degrees, above 0 and below 180
ts
// 2.6 m from the origin, 35 degrees round and 25 degrees up
slide.addModel3d({
  path: 'cube.glb',
  preview: { path: 'cube-render.png' },
  camera: { pos: { x: 1.3516, y: 1.0988, z: 1.9303 }, lookAt: { x: 0, y: 0, z: 0 }, fov: 45 },
})
  • With meterPerModelUnit scaling the model to 1 metre, the default camera frames a model that is about as deep and tall as it is wide.

  • For any other shape, PowerPoint keeps lookAt at the origin and moves the camera back until it contains the model's bounding sphere:

    text
    pos.z = |halfExtents| / maxExtent / sin(fov / 2)

    |halfExtents| is the length of the vector of half the box's width, height and depth. For a cube at 45 degrees that is (√3 / 2) / sin(22.5°), which is 2.2630334, the default. For a 1 by 1 by 8 box it is 1.32682.

  • Changing fov does not move the default pos, so a narrower view zooms in.

  • A pos, lookAt or up component that is missing or not a finite number throws model3d/invalid-camera. An fov outside its range throws model3d/invalid-fov.

Size the model ​

  • w and h default to 4 by 3 inches, and x and y to 0. A model has no aspect ratio of its own and the library does not open it, so set all four.
  • objectLock flags go on the model's frame. The preview picture carries a fixed set of locks.

Invalid input ​

Throws happen inside the addModel3d() call, as InvalidOptionError. A file that fails to load is reported when the deck is written.

ConditionWarns or throwsCode
neither data nor path is setthrowsmodel3d/missing-source
a pos, lookAt or up component is missing or not a finite numberthrowsmodel3d/invalid-camera
fov is not a finite number above 0 and below 180throwsmodel3d/invalid-fov
meterPerModelUnit is not a finite number above 0, or is below 0.0000005throwsmodel3d/invalid-scale
x, y, w or h is not a finite numberthrowscoord/non-finite
w or h is 0warns, and the zero is keptframe/zero-extent
no previewwarns, and the gray placeholder is embeddedmodel3d/preview-missing
preview.data has no base64 headerwarns, and the gray placeholder is embeddedpreview-image/missing-base64-header
path or preview.path fails to load (when written)throws MediaErrormedia/load-failed
the same, with onMediaError: 'placeholder'still throws MediaError, because a placeholder picture cannot stand in for the payloadmedia/load-failed
objectName is only whitespace, longer than 255 characters, or holds control characterswarnsobject-name/empty, object-name/too-long, object-name/control-characters

Errors and warnings covers the error classes and how to route warnings.

Limits ​

  • Linked models, which point at a file outside the package, cannot be authored.
  • A model is one .glb file. A .gltf with separate .bin and texture files is several files, so convert it to .glb first.
  • The library stores the bytes it is given as a .glb part without reading them. It measures no bounding box and does not notice a file that is not glTF.
  • The lighting is fixed: one ambient light and three point lights.
  • No animation settings are written, so an animation clip in the .glb has nothing to start it.
  • meterPerModelUnit keeps six decimal places.
  • pptx-ts/read has no typed accessor for a model.

Reading it back ​

ts
import { readFile } from 'node:fs/promises'
import { Presentation } from 'pptx-ts/read'

const deck = await Presentation.load(await readFile('engine.pptx'))
for (const slide of deck.slides) {
  for (const shape of slide.shapes) {
    if (shape.shapeType === 'graphicFrame') console.log(shape.name)
  }
}
  • Before export, slide.objects lists the model with type: "model3d" and canGroup: false.
  • pptx-ts/read loads the model as a graphicFrame shape whose name is its objectName. There is no accessor for the camera, the scale or the payload. See Kept but not decoded.
  • Loading and saving a deck leaves the slide, its relationships and the .glb part byte-identical. importSlide carries the model, its .glb and its preview into the target deck.
  • Inspect a package reports the model as a graphicFrame element with graphicKind: 'other'.
  • pptx-ts/script raises the graphicFrame.unknown fidelity note for a model, because it cannot write one back out.

See also ​