latest

@vctrl/core

Server-side 3D model processing for Node.js. This package provides the shared loading, optimization, and export pipeline used by other Vectreal packages.


Installation

npm install @vctrl/core
# or
pnpm add @vctrl/core

@vctrl/core targets Node.js only. It uses Sharp for server-side texture compression, which requires a native build.


Module overview

ModuleImport pathDescription
ModelLoader@vctrl/core/model-loaderLoad model files into glTF-Transform Document or Three.js scenes
ModelOptimizer@vctrl/core/model-optimizerRun optimization passes and export optimized output
ModelExporter@vctrl/core/model-exporterExport Document or Three.js objects to GLB/GLTF
SceneAsset@vctrl/coreScene asset serialization helpers and shared server payload types

ModelLoader

import { ModelLoader } from '@vctrl/core/model-loader'
import { readFile } from 'node:fs/promises'
 
const loader = new ModelLoader()
 
// From a file path
const result = await loader.loadFromFile('model.glb')
 
// From a Node.js Buffer
const buffer = await readFile('model.glb')
const resultFromBuffer = await loader.loadFromBuffer(
  new Uint8Array(buffer),
  'model.glb'
)
 
// Convert directly to Three.js scene
const sceneResult = await loader.loadToThreeJS('model.glb')

Primary methods

MethodDescription
loadFromFile(input)Load from file path (Node) or browser File
loadFromBuffer(buffer, fileName)Load from Uint8Array data
loadGLTFWithAssets(...) / loadGLTFWithFileAssets(...)Load GLTF with external resources
documentToThreeJS(document, modelResult)Convert glTF-Transform Document to Three.js scene
loadToThreeJS(input)Load and convert to Three.js scene
loadGLTFWithAssetsToThreeJS(...)GLTF + assets directly to Three.js
isSupportedFormat(fileName)Validate extension support
getSupportedExtensions()Return supported extensions

documentToThreeJS requires both the Document and the original ModelLoadResult metadata object:

const loaded = await loader.loadFromFile('model.glb')
const threeResult = await loader.documentToThreeJS(loaded.data, loaded)

ModelOptimizer

import { ModelOptimizer } from '@vctrl/core/model-optimizer'
 
const optimizer = new ModelOptimizer()
 
// Load a model into the optimizer
await optimizer.loadFromBuffer(modelBuffer)
 
// Run optimizations
await optimizer.simplify({ ratio: 0.5 })
await optimizer.deduplicate()
await optimizer.quantize({ quantizePosition: 14 })
await optimizer.compressTextures({ quality: 80 })
 
// Export the result
const optimizedBuffer = await optimizer.export()

Methods

MethodOptionsDescription
loadFromThreeJS(model)Load a Three.js scene into optimizer
loadFromBuffer(buf)Load GLB binary data
loadFromFile(path)Load from file path
loadFromJSON(json)Load serialized glTF JSON/resources
simplify(opts){ ratio: number }Mesh simplification (0.0–1.0)
deduplicate(opts)DedupOptionsRemove duplicate geometry/material data
quantize(opts)QuantizeOptionsReduce precision to reduce size
optimizeNormals(opts)NormalsOptionsRecompute/normalize normal data
compressTextures(opts)TextureCompressOptionsServer-side texture compression
optimizeAll(opts){ simplify?, dedup?, quantize?, normals?, textures? }Run batch optimization passes
getReport()Return before/after optimization metrics
export() / exportJSON()Export optimized GLB or JSON glTF document
hasModel() / reset()Model state utilities

Optimization options reference

simplify(options?: SimplifyOptions)

OptionTypeDefaultNotes
rationumber0.5Target simplification ratio. Lower values are more aggressive.
errornumber0.001Allowed geometric error threshold for simplification.

deduplicate(options?: DedupOptions)

OptionTypeNotes
texturesbooleanForwarded to glTF-Transform dedup()
materialsbooleanForwarded to glTF-Transform dedup()
meshesbooleanForwarded to glTF-Transform dedup()
accessorsbooleanForwarded to glTF-Transform dedup()

quantize(options?: QuantizeOptions)

OptionTypeNotes
quantizePositionnumberForwarded to glTF-Transform quantize()
quantizeNormalnumberForwarded to glTF-Transform quantize()
quantizeColornumberForwarded to glTF-Transform quantize()
quantizeTexcoordnumberForwarded to glTF-Transform quantize()

optimizeNormals(options?: NormalsOptions)

OptionTypeNotes
overwritebooleanRecompute normals even when normals already exist

compressTextures(options?: TextureCompressOptions)

OptionTypeCurrent behavior
resize[number, number]Passed to compressTexture()
targetFormat`'webp''jpeg'
qualitynumberPassed to compressTexture()
requestTimeoutMsnumberPresent in type but not consumed by current ModelOptimizer implementation
maxTextureUploadBytesnumberPresent in type but not consumed by current ModelOptimizer implementation
maxRetriesnumberPresent in type but not consumed by current ModelOptimizer implementation
maxConcurrentRequestsnumberPresent in type but not consumed by current ModelOptimizer implementation
serverOptionsServerOptionsAccepted in type but stripped before local Sharp compression

When Sharp is unavailable, compressTextures falls back to basic texture optimization (dedup + prune) instead of throwing.

optimizeAll(options?)

await optimizer.optimizeAll({
  simplify: { ratio: 0.6 },
  dedup: {},
  quantize: { quantizePosition: 14 },
  normals: { overwrite: false },
  textures: { targetFormat: 'webp', quality: 80 },
})

Execution order is fixed:

  1. simplify (unless false)
  2. deduplicate (unless false)
  3. quantize (unless false)
  4. optimizeNormals (unless false)
  5. compressTextures (only when textures is provided)

Calling optimizeAll() with no arguments runs simplify, dedup, quantize, and normals. Texture compression is opt-in.

getReport() return structure

getReport() includes:

  • originalSize, optimizedSize
  • compressionRatio (originalSize / optimizedSize)
  • appliedOptimizations
  • stats before/after metrics for vertices, triangles, materials, textures (size), texturesCount, textureResolutions, meshes, and nodes
const report = await optimizer.getReport()
console.log(report.stats.textureResolutions.before)
console.log(report.stats.textureResolutions.after)

ModelExporter

import { ModelExporter } from '@vctrl/core/model-exporter'
 
const exporter = new ModelExporter()
 
// Export Three.js scene as binary GLB
const glb = await exporter.exportThreeJSGLB(scene, {})
 
// Export Three.js scene as glTF + assets map
const gltf = await exporter.exportThreeJSGLTF(scene)
 
// Package glTF + assets into zip
const zip = await exporter.createZIPArchive(gltf, 'model')

Primary methods

MethodDescription
exportDocumentGLB(document)Export glTF-Transform Document to GLB
exportDocumentGLTF(document)Export Document to GLTF JSON + assets
exportThreeJSGLB(object, options)Export Three.js object to GLB
exportThreeJSGLTF(object)Export Three.js object to GLTF JSON + assets
createZIPArchive(result, baseName?)Bundle GLTF + assets into zip
saveToFile(result, filePath)Persist export result on Node filesystem

exportThreeJSGLB(object, options) accepts modifiedTextureResources in its options type, but that field is currently ignored for direct Three.js GLB export.


Use in API routes

@vctrl/core is used by the Vectreal Platform's server-side optimization endpoint. Here's a minimal example of using it in a Node.js API route:

import { ModelOptimizer } from '@vctrl/core/model-optimizer'
 
export async function POST(request: Request) {
  const formData = await request.formData()
  const file = formData.get('file') as File
 
  const buffer = Buffer.from(await file.arrayBuffer())
 
  const optimizer = new ModelOptimizer()
  await optimizer.loadFromBuffer(new Uint8Array(buffer))
  await optimizer.simplify({ ratio: 0.7 })
  await optimizer.compressTextures({ quality: 80 })
 
  const optimized = await optimizer.export()
 
  return new Response(optimized, {
    headers: { 'Content-Type': 'model/gltf-binary' },
  })
}

Requirements

RequirementVersion
Node.js18 or later
sharp^0.34

Source

Full source and README in packages/core/.